<div class="docs-content" data-render>
{% raw %}
# Consistency rules

These are hard, structural rules, not suggestions. They are derived from the **posts / feed page**, the reference implementation and the most structurally and visually correct page in the application. Other pages are refactored to match it; new pages follow these rules from the start.

The header, breadcrumb, content root, and footer are *shared infrastructure*. A page supplies its content and its stylesheet; it never rebuilds the frame.

## The reference skeleton

This is the posts page reduced to its structure. A content page looks like this and nothing else:

```html
{% extends "base.html" %}

{% block extra_head %}
&lt;link rel="stylesheet" href="{{ static_url('/static/css/feed.css') }}"&gt;
{% endblock %}

{% block content %}
&lt;h1 class="sr-only"&gt;Feed&lt;/h1&gt;

&lt;div class="feed-layout"&gt;            {# exactly one layout container #}
    &lt;aside class="sidebar-card"&gt; ... &lt;/aside&gt;
    &lt;div class="feed-main"&gt; ... &lt;/div&gt;
    &lt;aside class="feed-right"&gt; ... &lt;/aside&gt;
&lt;/div&gt;
{% endblock %}
```

The header, breadcrumb, `<main class="page">` wrapper, and footer are all supplied by `base.html`. The template above never appears to contain them, and never should.

## Hard rules

### 1. Extend the base template

Every page is `{% extends "base.html" %}`. Page CSS is added only through `{% block extra_head %}`; page JS only through `{% block extra_js %}`. A page never includes its own `<html>`, `<head>`, or `<body>`, and never links `variables.css` / `base.css` / `components.css` (the base already does).

### 2. The header is shared, never re-implemented

The top nav is `base.html`'s `.topnav`. Pages do not build their own header or navigation bar; the active link is derived from the request path inside `base.html`. A page-level toolbar (tabs, filters) lives *inside the content*, like the feed's `.feed-nav` strip, below the breadcrumb, never as a second site header.

### 3. Breadcrumbs are mandatory (this is the most-broken rule)

A page **must** pass at least two breadcrumbs (Home plus the current page) through its SEO context:

```python
seo_ctx = list_page_seo(
    request,
    title="Projects",
    breadcrumbs=[{"name": "Home", "url": "/feed"}, {"name": "Projects", "url": "/projects"}],
)
```

This is primarily for SEO and orientation. Clearing the fixed top nav is handled once, globally, by `body { padding-top: var(--nav-height) }`, so a page is never hidden behind the header whether or not it has a breadcrumb. Still supply two or more crumbs on any nested page for navigation and breadcrumb structured data.

### 4. All content lives in the single content root

`base.html` already wraps the `content` block in `<main class="page">` (`max-width: 1200px`, centred, `padding: 1rem`). Therefore a page:

- puts everything inside one top-level layout container (a grid like `.feed-layout`, or a single column);
- never adds a second centred / `max-width` wrapper inside `.page`;
- never re-declares the maximum content width.

See [Layout](/docs/styles-layout.html) for the approved layout containers.

### 5. Multi-column structure is grid, and it collapses

Side columns are `<aside>`; the fluid centre column sets `min-width: 0`. The grid collapses to a single fluid column at `1024px` and the right rail is hidden. See [Responsiveness](/docs/styles-responsiveness.html).

### 6. One H1 per page

Each page has exactly one `<h1>`. When the heading is visually redundant (the feed's title), keep it for accessibility with `class="sr-only"` rather than removing it.

### 7. The footer is shared

The footer is `base.html`'s `.site-footer`, rendered from the `footer` block. Pages do not build their own footer. Override the `footer` block only with a deliberate reason (for example, a full-bleed tool page) and document why.

### 8. Cards and surfaces use tokens

Content sits on cards built from tokens: `--bg-card`, a `--border` hairline, `--radius-lg`, `1rem` padding (the shared `.card` recipe). Colours, spacing, radii, shadows, and z-indexes are always tokens, never literals: global tokens are referenced bare (`var(--accent)`, never `var(--accent, #hex)`), accent tints compose `rgba(var(--accent-rgb), α)`, and stacking uses the `--z-*` scale. See [Colors](/docs/styles-colors.html) and [Layout](/docs/styles-layout.html).

## Page author checklist

- [ ] `{% extends "base.html" %}`; CSS via `extra_head`, JS via `extra_js`.
- [ ] Two or more breadcrumbs passed in the SEO context (content clears the nav).
- [ ] Exactly one layout container inside `.page`; no second `max-width` wrapper.
- [ ] Multi-column = grid with `<aside>` rails, fluid column has `min-width: 0`, card-grid tracks are `minmax(0, 1fr)`, collapses at `1024px`.
- [ ] One `<h1>` (use `.sr-only` if visually redundant).
- [ ] No custom header or footer; both come from `base.html`.
- [ ] Only tokens for colour, spacing, radius, shadow, and z-index; no `var()` fallback values.
- [ ] Standard breakpoints only (`1024 / 768 / 480 / 360`).
- [ ] No page-level `prefers-reduced-motion` query unless the page must change content, not just motion (the global `base.css` rule already covers motion).
{% endraw %}
</div>

<style>
.sg-anatomy { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.sg-anatomy-row { padding: 0.625rem 0.875rem; font-size: 0.8125rem; border-bottom: 1px solid var(--border); }
.sg-anatomy-row:last-child { border-bottom: none; }
.sg-anatomy-nav { background: var(--bg-secondary); color: var(--text-primary); font-weight: 600; }
.sg-anatomy-crumb { background: var(--bg-primary); color: var(--text-muted); font-size: 0.75rem; }
.sg-anatomy-main { background: var(--bg-card); color: var(--text-secondary); padding-top: 1rem; padding-bottom: 1rem; }
.sg-anatomy-foot { background: var(--bg-primary); color: var(--text-muted); text-align: center; font-size: 0.75rem; }
.sg-anatomy code { font-size: 0.75rem; color: var(--accent); }
.sg-anatomy-tag { float: right; font-family: var(--font-mono); font-size: 0.6875rem; color: var(--text-muted); }
.sg-anatomy-brand-accent { color: var(--accent); }
</style>

<div class="component-demo">
    <div class="component-demo-title">Anatomy of a page (what base.html owns vs what the page supplies)</div>
    <div class="sg-anatomy">
        <div class="sg-anatomy-row sg-anatomy-nav">Dev<span class="sg-anatomy-brand-accent">Place</span> &middot; Home Posts News Gists Projects<span class="sg-anatomy-tag">base.html: .topnav (fixed)</span></div>
        <div class="sg-anatomy-row sg-anatomy-crumb">Home / Projects<span class="sg-anatomy-tag">base.html: .breadcrumb (clears the nav)</span></div>
        <div class="sg-anatomy-row sg-anatomy-main">Your content, inside one layout container.<span class="sg-anatomy-tag">page: block content, inside .page</span></div>
        <div class="sg-anatomy-row sg-anatomy-foot">DevPlace &middot; footer links<span class="sg-anatomy-tag">base.html: .site-footer</span></div>
    </div>
</div>
