# Themes and appearance

There are two independent ways to change how Quaeriva looks:

1. **Appearance settings** — colours, fonts, sizes, logo, custom CSS. No files, no FTP, everything from the admin panel. This covers most of what a site owner wants.
2. **A theme** — files on disk that can override page templates and add stylesheets. Only needed when settings are not enough.

Start with the first. Reach for the second only when you need to change page *structure*.

---

## 1. Appearance settings

**Administration → Appearance**, six tabs:

| Tab | Contains |
|---|---|
| Branding | Active theme, brand text, logo URL, favicon URL |
| Colours | Nine colours — primary, primary dark, accent, surface, surface soft, ink, ink muted, line, danger |
| Type and layout | Font stack, base font size, two corner radii, content width, narrow width |
| Page regions | The nine widget regions and what each holds |
| Custom CSS | Free-form CSS applied after everything else |
| Import and export | The whole appearance as an editable JSON package |

Values are emitted as CSS custom properties in a `<style>` block in the page head. **No files are written**, which is what makes this work on hosts that do not give PHP write access to the web root.

Font stacks available: `system`, `inter`, `cairo`, `noto_kufi`. `noto_sans` is retained only as a legacy stored alias and resolves to `cairo`. Cairo and Noto Kufi Arabic cover Arabic UI; the active locale still controls RTL/LTR.

### Export and import

Export produces a JSON package containing the theme manifest and every appearance value. You can edit it in any text editor and import it back — useful for moving a look between a staging and a live site, or for keeping a palette under version control.

**The package can never contain PHP, JavaScript or template files.** Only settings values are imported, and every one is re-validated on the way in. A package with an invalid colour is rejected whole, not partially applied.

### Custom CSS

Applied after the theme stylesheets, so it wins. It is not sanitised — it is CSS authored by an administrator — but `</style` is rejected so it cannot break out of its block, and there is a size cap.

---

## 2. Page regions

A region is a named slot in the page that can hold widgets. Nine exist:

| Region | Where it renders |
|---|---|
| `header_top` | A strip above the header bar |
| `nav_extra` | Extra items inside the navigation |
| `homepage_main` | The home page body, below the question list |
| `home_sidebar` | Beside the home page question list |
| `question_sidebar` | Beside a question page |
| `footer_col_1/2/3` | Footer columns |
| `footer_bottom` | A bar below the footer |

**A region with no widgets renders the built-in layout instead.** That is what makes this safe to adopt gradually: an upgraded site looks identical until you put something in a region.

Note the two sidebars are page-specific — `home_sidebar` appears only on the home page, `question_sidebar` only on question pages.

### Widgets

Twenty types: `html_block`, `latest_questions`, `categories`, `top_contributors`, `most_viewed`, `most_useful`, `most_answered`, `unanswered`, `active_tags`, `trending`, `ad_slot`, `link_list`, `about_box`, `social_links`, `cta_button`, `site_stats`, `category_questions`, `search_box`, `image_banner`, and `text_block`.

Each type's options are generated as real form fields — number inputs carry their real limits, the category field is a category picker. Reorder widgets inside a region by dragging, or with the up/down buttons beside each one; the order is only saved when you press **Save order**.

`html_block` content passes through the HTML sanitiser: paragraphs, lists, links, headings and emphasis survive; scripts, styles and event handlers do not. This is deliberate and is not configurable.

`ad_slot` renders the homepage advertisement placement and only works in `homepage_main`. Placed elsewhere it renders nothing rather than duplicating the advertisement.

---

## 3. Menus

**Administration → Navigation menus** replaces the hardcoded header links. Each item has a label, a URL, an order, an audience (everyone / signed-out / signed-in / staff), an optional language restriction, and a new-tab flag.

An item restricted to an audience is **absent from the HTML entirely** for everyone else, not hidden with CSS.

Items seeded at upgrade carry a translation key rather than a literal label, so they stay translated in every enabled language. Type your own label and it replaces that permanently for that item.

URLs are validated with the same rules applied to user links: relative paths, fragments, and `http`/`https` only. Never `javascript:` or `data:`.

---

## 4. Building a theme

A theme is a directory under `public/themes/`:

```
public/themes/mytheme/
  theme.json
  assets/css/theme.css        loaded after the core stylesheet
  assets/css/rtl.css          optional, loaded only in RTL
  assets/css/ltr.css          optional, loaded only in LTR
  assets/js/*.js              optional
  views/                      optional template overrides
  lang/ar.php                 optional theme strings
  lang/en.php
```

### `theme.json`

```json
{
  "name": "mytheme",
  "version": "1.0.0",
  "author": "Your name",
  "directions": ["rtl", "ltr"],
  "min_core": "0.7.1",
  "assets": {
    "css": ["assets/css/theme.css"],
    "js": []
  }
}
```

`name` must match the directory name. `directions` declares the writing directions the theme supports; a theme may be RTL-only, LTR-only, or both, but activation is refused when it does not cover every direction required by the site's enabled languages. `min_core` is enforced at activation time, so a theme requiring a newer Quaeriva Core falls back to the default theme.

### Template overrides

Any file under `views/` replaces the core template of the same path. To restyle the question page, copy `app/views/question.php` to `public/themes/mytheme/views/question.php` and edit the copy. Templates you do not override continue to come from core — so a theme that only wants a different footer overrides only the layout.

There is no parent/child theme chain: the lookup is your theme, then core.

### Theme strings

`lang/<locale>.php` returns a flat array. Keys are automatically namespaced under `theme.`, so a theme **cannot** redefine a core string, and an administrator's translation override still wins over the theme's wording.

```php
<?php
return [
    'tagline' => 'Ask well, answer better',
];
```

Read it in a template as `theme.tagline`.

### CSS rules for theme authors

**Use logical properties only** — `margin-inline-start`, not `margin-left`. Quaeriva serves Arabic and English from one stylesheet, and a physical property is invisible in the direction it was written for and silently wrong in the other. `php tests/Public/task51.php` fails the build on any stylesheet, including a theme's, that uses one.

Style against the existing custom properties (`--teal`, `--ink`, `--surface`, `--line`, `--radius-md`, `--container`…) so the appearance settings keep working in your theme.

---

## 5. Installing a theme

**Upload it by FTP** into `public/themes/`, then select it under Administration → Appearance → Branding.

There is deliberately **no browser upload for themes**. A theme is PHP that executes inside your site; accepting one through a web form is the most common route to a compromised shared-hosting install. FTP access is already the boundary that decides who can run code on your server, and this keeps it that way.

If the active theme's directory disappears, the site falls back to the default theme and keeps running rather than failing.

---

## 6. What this system does not do

Stated plainly so you can plan around it:

- **No live preview.** Appearance changes are save-then-reload.
- **No child themes.** Overriding one template means maintaining your copy against core changes.
- **Plugins and hooks exist, but plugin-provided widget registration is not implemented yet.** Adding a new widget type still requires a Core change until the widget registry becomes extensible.
- **No drag-and-drop menu ordering** — menus use a numeric order field. Widgets do have drag ordering.
- **No nested menu items.**

---

Quaeriva has two layers: trusted filesystem themes can override templates such as `home.php`, `question.php` and `topic.php`; Theme Profiles manage safe JSON design tokens/layout presets from administration. PHP theme upload through the browser is intentionally unsupported.

## Choosing a theme: two surfaces, two different things

Appearance has two places that both look like "pick a theme". They are not duplicates:

| Tab | Chooses | Stored in | What changes |
|---|---|---|---|
| **Branding** | The theme **directory** | `theme.active` | Page structure — the view overrides, layouts and markup |
| **Profiles** | The theme **profile** | `theme.profile_id`, `theme.tokens` | Palette, typography, shape and layout presets |

Structure and palette are deliberately separable: an operator can keep a layout and restyle it, or reuse a palette across layouts.

To keep the common case simple, selecting a theme in **Branding** also applies that theme's bundled profile when one exists at `public/themes/{slug}/{slug}.json`. Selecting a different profile afterwards keeps the structure and changes only the tokens — so a mismatched pairing is possible and is a legitimate choice, not a bug.

## Logo and favicon

Both accept either a URL or a direct upload.

- Allowed types: PNG, JPG, GIF, WebP, ICO. **SVG is not accepted** — it can carry script.
- Maximum size: 2 MB.
- The stored value is always a plain URL, so nothing downstream needs to know whether it came from an upload.
- Uploads land in `public/uploads/branding/` under a generated name, with the extension taken from the **decoded image type** rather than the submitted filename.
- That directory ships with an `.htaccess` that disables the PHP engine and denies script extensions, so a file that is renamed later still cannot execute.

An upload replaces the URL field for that slot. Leaving both empty falls back to the text brand mark.
