# uzinaduzina · documentation

This file is the single canonical reference for the website at `uzinaduzina.org`. It covers what the site is, how it is structured, how to add content, how it is built, and how it is deployed. `README.md` is a shorter quickstart for adding notes; everything below is the long form.

---

## 1 · What this is

uzinaduzina is a non-profit cultural association in Cluj-Napoca, Romania, founded in 2008 and rebooted in 2020. The website is a stacked-notes archive of its work: 18 projects, 11 fundamentals (the manifesto), 7 team profiles, 1 (so far) partner profile, plus organisational notes and curiosities. Forty notes in total. Primary domain: `uzinaduzina.org`. Repo: `github.com/liviupop/uzduzuz26`. Deployed on Cloudflare Workers Static Assets.

The audience priority is unusual:

1. **AI crawlers** (Claude, GPTBot, Perplexity, Common Crawl) — the first reader of any page is, statistically, a machine.
2. **European grant evaluators** (Erasmus+, CERV, AFCN, Botnar, etc.).
3. **Partners** — institutions, NGOs, artists, schools.
4. **Project participants and the public.**
5. **Internal team.**

This priority shapes everything: the markdown source is served alongside the rendered page, JSON-LD-style structured data lives in the front matter, `llms.txt` indexes the site for LLMs at the root, and the page works with JavaScript disabled (the stacked-columns interaction is the enhancement; the content is the floor).

---

## 2 · Architecture at a glance

```
┌────────────────────────────────────────────────────────────┐
│ index.html  ←  the only HTML file. A shell with header,   │
│                sidebar, and a horizontally-scrolling stack │
│                of columns.                                 │
└────────────────────────────────────────────────────────────┘
            ↓ loads on boot
┌────────────────────────────────────────────────────────────┐
│ assets/tokens.css   colour, type, spacing, fonts.         │
│ assets/site.css     all component styles.                 │
│ assets/data.js      partners + projects network data      │
│                     (drives the constellation viz).        │
│ notes.js            structurally rich pages: home, indexes,│
│                     manifesto outer page, contact.         │
│ app.js              renderer + URL-state router. Loads .md │
│                     files on demand for editorial pages.   │
└────────────────────────────────────────────────────────────┘
            ↓ at runtime
┌────────────────────────────────────────────────────────────┐
│ content/<slug>.md   editorial pages (projects, team,       │
│                     partners, manifesto principles,        │
│                     curiosities, who-we-are).              │
│ content/_index.json index of every .md, generated by       │
│                     regen.py at edit-time.                 │
└────────────────────────────────────────────────────────────┘
```

There is no build framework, no React, no bundler. `regen.py` is the only build-time script and it has no dependencies beyond a stock Python 3.

---

## 3 · File layout

```
uzduz25/
├── index.html              the shell
├── app.js                  renderer, URL router, markdown loader,
│                           constellation viz, "next principle" nav
├── notes.js                structured pages (home, indexes, contact)
├── regen.py                indexer; walks content/, writes _index.json
│
├── assets/
│   ├── tokens.css          colour, type, spacing, motion vars
│   ├── site.css            component styles (~1500 lines, organised
│   │                       by section header comments)
│   ├── data.js             projects + partners network for constellation
│   ├── logo-uzinaduzina.png
│   ├── illustration-balcony.svg
│   ├── illustration-press.svg
│   ├── illustration-radio.svg
│   └── diagrams/
│       ├── dunbar-compact.html         embedded in who-we-are
│       ├── dunbar-scaling.html         the 1:3 ratio diagram
│       └── self-governing-workshop.html  Pattern 80 floor plan
│
├── content/
│   ├── _index.json                     auto-generated by regen.py
│   ├── who-we-are.md                   organisation note
│   ├── manifesto-*.md                  11 principle pages (the fundamentals)
│   ├── project-*.md                    18 project notes
│   ├── team-*.md                       7 team profiles
│   ├── partner-*.md                    partner profiles (CCIF so far)
│   └── curiosity-*.md                  2 curiosities
│
├── fonts/
│   ├── Bitter-VariableFont_wght.ttf
│   └── Bitter-Italic-VariableFont_wght.ttf
│
├── llms.txt                AI crawler index
├── robots.txt              pro-AI policy (allows GPTBot, ClaudeBot, …)
├── sitemap.xml             canonical URLs for traditional crawlers
├── README.md               quickstart for adding notes
└── DOCUMENTATION.md        this file
```

Excluded by `.gitignore`: `.DS_Store`, `uploads/` (old screenshots), `ds/` (stale duplicate of the design system), `.claude/`, `__pycache__/`, `.wrangler/`.

---

## 4 · Content model

The site recognises seven note types. The type drives the auto-list filters on index pages and the front-matter conventions used in each file.

| Type | Filename pattern | Count | Where it appears |
|---|---|---|---|
| `org` | `who-we-are.md` (and structured pages in notes.js) | 1 + 4 | home, who-we-are, contact, projects/team/partners/fundamentals indexes |
| `project` | `content/project-<slug>.md` | 18 | projects index, constellation, "Read alongside" lists |
| `person` | `content/team-<slug>.md` | 7 | team index |
| `partner` | `content/partner-<slug>.md` | 1 | partners constellation |
| `principle` | `content/manifesto-<slug>.md` | 11 | fundamentals index, "next principle" nav |
| `curiosity` | `content/curiosity-<slug>.md` | 2 | linked from notes that reference them |
| `note` | `content/<anything>.md` | 0 (used for one-offs) | wherever linked |

The filename prefix (`project-`, `team-`, `partner-`, `manifesto-`, `curiosity-`) tells the indexer the type. You can override with an explicit `type:` field in the front matter, but matching prefix to type keeps the convention obvious.

---

## 5 · Editorial conventions

Non-negotiable, applied across every file (Romanian and English alike):

- **No em-dashes (—).** Use `:`, `;`, `,`, `()`, or start a new sentence. En-dashes are fine for date ranges (`2020–2024`).
- **Romanian diacritics with comma-below**: `ă â î ș ț` (U+0219, U+021B). Never the cedilla forms.
- **The wordmark is always `uzinaduzina`**: lowercase, one word, never split, never capitalised.
- **Sentence case for headings.** Never Title Case, never ALL CAPS.
- **No emoji**, no decorative SVGs of abstract concepts, no stock photography, no carousels, no popups, no autoplay video, no gradients, no neumorphism, no glassmorphism, no "trusted by" strips, no hero sections, no recommendation algorithms, no login walls.

Voice rules:

- **Reflexive, not didactic.** "We tried X, learned Y, are unsure about Z" beats "Five tips for...".
- **Warm but precise.** Real numbers, real names, real dates.
- **Provisional.** Closings are open. "We will probably keep doing this" beats "Mission accomplished".
- **Honest argumentation.** Counter-evidence is named, not buried.
- **Pro-European, explicit.** No coyness about funding sources or partners.

Vocabulary cues:

| Avoid | Prefer |
|---|---|
| solutions, leverage, empower | tried, found, helped, taught |
| innovative, cutting-edge | new to us, recent |
| stakeholders | partners, participants, evaluators |
| impact (as noun) | what changed, who came back |
| journey, story (in the marketing sense) | what happened, what we did |

---

## 6 · Visual design

All tokens are CSS custom properties in `assets/tokens.css`. Lift them; do not redecide.

**Colour:**
- Background `--bg: #FAF6F0` (cream). Never `#FFFFFF`.
- Paper-tinted variants: `--bg-paper: #F5EFE5`, `--bg-sunk: #EDE5D6`.
- Text `--ink: #1A1F2E` (dark navy). Never `#000000`.
- Muted text: `--ink-muted: #5B6178`.
- Rules: `--rule: #D9D0BE`.
- Three accents: `--accent: #4338D6` (indigo), `--ochre: #C99745`, `--green: #5A7A4E`. Each has a soft variant for callouts and tints.

**Typography:** the brand family is **Bitter** (variable serif, both upright and italic), self-hosted under `fonts/`. Bitter handles display *and* body — no second family on the page. **Inter** is loaded from Google Fonts for UI metadata only (kickers, breadcrumbs, pill labels). **JetBrains Mono** for code and numerical data.

**Type scale** anchors on a 1.250 ratio: H1 44 px, H2 28 px, H3 22 px, body 19 px, lede 22 px italic, meta 13 px Inter.

**Spacing:** `4 / 8 / 12 / 16 / 24 / 32 / 48 / 64 / 96` px, exposed as `--space-1` through `--space-9`.

**Motion:** all transitions are short, single-axis, `cubic-bezier(0.2, 0, 0, 1)`, ≤ 380 ms. No spring physics. Every animation respects `prefers-reduced-motion: reduce`. The most prominent animation is the column slide-in at 380 ms; the most discreet is the home stamp's 4° rotation.

**Elevation:** none in the modern sense. The only "shadow" is a 1px hairline on the right edge of an open note column. No drop shadows anywhere else.

**Icons:** the site does not use a decorative icon system. The single exception is the **mobile menu**, which uses a tiny inlined library of seven Solar duotone SVGs (the `SOLAR_DUOTONE` map at the top of `app.js`): `home`, `people`, `document`, `folder`, `person`, `planet`, `letter`. They are duotone for parity with the editorial restraint elsewhere — primary fill at full opacity, secondary fill at 0.5. The `solarIcon(name)` helper returns an inline `<svg>` ready to drop into a button. To add a new icon, append to `SOLAR_DUOTONE` with the SVG path data and reference it by name.

---

## 7 · Interaction model

The stacked-notes pattern, after Andy Matuschak's `notes.andymatuschak.org`. Each link in column N opens a new column N+1 to its right. The whole stack lives in the URL.

### URL structure

```
/?n=home,manifesto,project-democraicy
       └─0──┘ └────1───┘ └──────2──────┘
        spine    spine     active (default = last)

/?n=home,manifesto,project-democraicy&a=1
                              ↑       └─┘ override active index
```

`?n=` is a comma-separated stack of slugs (left-to-right). `&a=N` is an optional 0-based index of which column is active. When omitted, the rightmost column is active.

### Per-state behaviour

- **Click a link inside an active column.** The clicked target opens to the right; everything previously to the right of that column is dropped.
- **Click an open column's spine** (any non-active column). That column promotes to active; the previously-active note collapses to a spine in place. Nothing is dropped.
- **Click the same link twice / a link already in the stack.** The existing column promotes to active (no duplicate).
- **Click × on a column.** That single column is removed; activeIndex shifts to the right neighbour, falling back to the left.
- **Browser back/forward.** Each user-initiated state change uses `pushState`; popstate just re-reads the URL and renders. The sequence works as expected.

### Mobile (≤ 720 px)

The chrome bar stays fixed at the top. The active note fills the centre and scrolls with the body. All other columns pin together at the **bottom of the viewport** as a stack of fixed strips (one row each, set via `--spine-index` on each strip). Tapping a bottom strip promotes it to the active position; the previously-active note joins the bottom stack. The page auto-scrolls to top on each activation so the new active's title is visible immediately.

**Hamburger / mobile menu.** Below 720 px the desktop chrome-nav (`who we are · fundamentals · projects · team · partners · contact`) is hidden. A square button with a Solar duotone icon takes its place in the top-right corner. Tapping it opens a fixed two-column grid of icon-and-label tiles; tapping outside, hitting Escape, or selecting any tile closes it. The menu is rendered in `index.html` as `<nav class="mobile-menu" hidden>` and populated dynamically from the same `PRIMARY_TABS` array in `app.js` that drives the desktop nav, so labels stay in sync. Each tile shows a Solar duotone icon (`people`, `document`, `folder`, `person`, `planet`, `letter`) plus its label; the active tab is highlighted in the indigo soft accent.

### History persistence

The URL is the source of truth for state. A user can paste any `?n=...&a=...` URL into a new tab and arrive at the exact stack and active column. Cloudflare's caching plays well with this since the only HTML on the wire is `index.html`; everything else is reconstructed client-side.

---

## 8 · Adding content

The whole add-a-note workflow:

```sh
# 1. Drop a file with the right slug
$EDITOR content/project-my-new-thing.md

# 2. Regenerate the auto-index
python3 regen.py

# 3. Refresh the browser. Done.
```

### Front matter

```yaml
---
title: "Display title"
slug: project-my-new-thing      # must match filename without .md
type: project                   # see Content model above
kicker: "project · 2026 · lead" # small label above the title
accent: indigo                  # indigo | ochre | green
role: lead                      # for projects: lead | partner | beneficiary
order: 17                       # lower = earlier in index. Default: 99.
summary: "One-line tagline shown in the index row."
pills:
  - ongoing|on                  # the |on suffix highlights as "live"
  - 2026
  - Erasmus+ KA220
---
```

| Field | Purpose |
|---|---|
| `title` | Title at the top of the column. **Required.** |
| `slug` | Note id. Must match the filename. |
| `type` | `project` / `person` / `partner` / `principle` / `curiosity` / `org`. Inferred from filename prefix if absent. |
| `kicker` | Small label above the title; also the sub-line in the index row. |
| `accent` | `indigo` (default), `ochre`, or `green`. Tints lede rule, headings, links. |
| `subtitle` | Optional one-line subtitle in the note view. |
| `summary` | Tagline shown in the index row. Falls back to nothing if missing. |
| `role` | For projects: `lead` / `partner` / `beneficiary`. The projects index splits on this. |
| `order` | Integer. Lower = earlier in the index. Defaults to 99. |
| `pills` | List of pill labels. Append `|on` for the highlighted "live" variant. |
| `year`, `period`, `funder`, `budget`, `partners` | Optional. Indexed for filters but not currently displayed in cards. |

### Body

Plain GitHub-flavoured Markdown. The first paragraph after the optional `# H1` becomes the **lede** (italic, accent-coloured left rule).

Supported:

- `## h2`, `### h3`, `#### h4`
- Paragraphs, blank-line-separated.
- `> blockquote` (consecutive `>` lines join into one quote).
- `- item`, `* item`, `1. item` lists.
- `**bold**`, `*italic*`, `` `code` ``, `[label](url)`, `![alt](src)`.
- Tables with `| ... |` syntax.
- `---` for horizontal rule.
- Raw HTML lines (e.g. `<iframe>`, `<details>`, `<video>`) pass through verbatim.

### Internal link routing

Links to other notes accept several shapes; the parser normalises them:

| Source markdown | Resolves to slug |
|---|---|
| `[CCIF](partner-ccif)` | `partner-ccif` |
| `[CCIF](/partners/ccif)` | `partner-ccif` (the `partners/` directory prefix maps to `partner-`) |
| `[principle](/manifesto/calm-technologies)` | `manifesto-calm-technologies` |
| `[Liviu](/team/liviu-pop)` | `team-liviu-pop` |
| `[democraicy](/projects/democraicy)` | `project-democraicy` |
| `[curiosity](/curiosities/dunbar-alexander-and-the-dozen)` | `curiosity-dunbar-alexander-and-the-dozen` |
| `[external](https://...)` | external link, opens in new tab |

The runtime parser handles both the bare-slug form and the `/section/slug` form, mapping plural directory segments (`projects/`, `manifesto/`, etc.) to their singular file prefixes. Use whichever shape you prefer.

---

## 9 · Adding images and media

1. Drop the image file into `assets/images/` (create the folder if it doesn't exist; `.gitignore` does not exclude it).
2. Reference it in Markdown:
   ```markdown
   ![Caption text describing the photo](assets/images/cabinet-prototype-04.jpg)
   ```
3. The renderer wraps the image in `<figure>` with the alt text used as `<figcaption>`. Use a real caption that makes sense to a reader; it doubles as alt text for screen readers and AI crawlers.

Conventions:

- File names lowercase and kebab-case (`goana-dupa-meteor-perseids-2025.jpg`). No spaces.
- Optimise before committing — aim for ≤ 200 KB per image.
- Paths are relative to `index.html` at the site root, so `assets/images/foo.jpg` works from any markdown file.
- Multiple images one per line stack vertically with a 32-px gap.

For video, embed via raw HTML (`<video>` or YouTube `<iframe>`) — the parser passes those through.

---

## 10 · Embedded HTML / iframes / diagrams

Three interactive SVG diagrams live in `assets/diagrams/`:

| File | Where it appears | What it shows |
|---|---|---|
| `dunbar-compact.html` | embedded in `who-we-are.md` | The four Dunbar layers with the dozen highlighted |
| `dunbar-scaling.html` | linked from the Dunbar curiosity | The 1:3 ratio across layers (Zhou et al. 2005) |
| `self-governing-workshop.html` | linked from the Dunbar curiosity | Christopher Alexander's Pattern 80 floor plan |

Each is a self-contained HTML file: includes its own `<style>` block, references `../tokens.css` for design tokens, has its own SVG markup with CSS-driven animations, and respects `prefers-reduced-motion`. They render at any width without breaking.

To embed one in a markdown note:

```markdown
<iframe src="/assets/diagrams/dunbar-compact.html" 
        style="width:100%; height:480px; border:1px solid var(--rule); border-radius: var(--radius-3); background: var(--bg-paper); margin: var(--space-5) 0;" 
        title="The four Dunbar layers, with the dozen highlighted"></iframe>
```

The markdown parser passes raw HTML through verbatim. The styling on the iframe inherits the same tokens as the parent page; the iframe gets a 1px rule border and a paper background so it reads as a contained artefact, not a competing card.

To author a new diagram, copy `dunbar-compact.html` as a template, change the SVG, and drop it in `assets/diagrams/`. Then iframe it from any markdown file.

---

## 11 · The auto-index (`regen.py`)

Browsers cannot list a directory at runtime, so the site needs a static manifest of available `.md` files. `regen.py` builds it.

### What it does

1. Walks `content/*.md` (skips files starting with `_`).
2. Parses YAML-style front matter from each file (its own tiny parser, no PyYAML dep).
3. Extracts the **first body paragraph** and trims to the **first sentence** as `excerpt` (used as a fallback tagline if `summary` is empty).
4. Writes one entry per note to `content/_index.json`, sorted by `(type, order, slug)`.

### What gets indexed

Per entry:
```json
{
  "slug": "project-democraicy",
  "type": "project",
  "title": "democraicy: algorithms with civic conscience",
  "kicker": "concept document · 2020 · unfunded",
  "accent": "indigo",
  "subtitle": "",
  "summary": "An early concept document on algorithms with civic conscience...",
  "excerpt": "A project concept written in November 2020.",
  "year": "2020",
  "role": "lead",
  "order": 7,
  "pills": ["2020", "concept", "unfunded"]
}
```

### How the runtime uses it

`app.js` defines an `auto-list` block kind that takes a filter object (`{ type: 'project', role: 'lead' }` or similar) and renders matching notes from `_index.json` as a list of click-through rows. Used by the projects/team/manifesto/partners indexes in `notes.js`. So adding a project = drop md + run regen + refresh.

### When to run it

Whenever you add, rename, or remove a `.md` file in `content/`. Editing existing files doesn't strictly require a regen unless you changed front-matter fields that affect the index (`title`, `kicker`, `summary`, `order`, `role`, etc.). When in doubt, run it — it takes < 1 s and has no side effects.

---

## 12 · The constellation graph

The partners page renders an SVG network of every project and partner. Data lives in `assets/data.js`:

```js
window.UZD_DATA = {
  name: 'uzinaduzina',
  projects: [
    { slug: 'green-genesis-startup', title: '...', year: 2022, role: 'lead',
      funder: 'erasmus-plus-ka220-you', budget: 205144,
      partners: ['co-labory','ccif','innovation-hive','acd-la-hoya','udruga-murtila'],
      country: 'RO', blurb: '...' },
    /* ...18 more... */
  ],
  partners: [
    { slug: 'ccif', name: 'CCIF Cyprus', country: 'CY', city: 'Paphos',
      projects: ['green-genesis-startup','modeling-the-future', /* ... */],
      note: 'Strategic partner. Five projects together since 2022.' },
    /* ...10 more... */
  ],
  trackNames: { ... },     // pretty labels for track filter chips
  countryNames: { ... },   // ISO → display name
};
```

### Layout algorithm

- uzinaduzina at centre `(cx, cy)`.
- Partners distributed on a ring of radius 250, sorted by `projects.length` descending. Largest tie at the top.
- Projects positioned 55% of the way from centre to the centroid of their listed partners. Orphans (no partners listed) form a tight inner ring at radius 90.
- Edges: thin grey hairlines from centre to each partner, and from each project to each of its partners. Hovered/clicked nodes promote to indigo accent.
- Project radius keys to role (`lead` 9, `partner` 7, `beneficiary` 6). Partner radius scales with `min(projects.length, 6) * 2 + 16`.

Hover any node for a side panel; double-click to open the corresponding project or partner note in a new column.

### Adding a project to the graph

1. Add an entry to `projects:` in `data.js` with the same `slug` as your `content/project-<slug>.md`.
2. Refresh — the constellation picks it up at next page load.

(`regen.py` does not touch `data.js`. The two indexes are independent: `_index.json` drives list rendering, `data.js` drives the network graph.)

---

## 13 · Local development

```sh
cd uzduz25
python3 -m http.server 8080
# open http://localhost:8080
```

Any static server works (`python3 -m http.server`, `npx serve`, `caddy file-server`, etc.). The markdown loader uses `fetch()`, which browsers block on `file://` URLs, so a real server is required — opening `index.html` by double-click won't load any markdown content. The site detects this and shows a clear error in the placeholder note.

There is **no build step** required for development beyond running `regen.py` after content edits. There is no `package.json`, no `node_modules`, no JavaScript bundler.

---

## 14 · Deployment

The repo is connected to Cloudflare Workers Static Assets:

- Source: `github.com/liviupop/uzduzuz26`, branch `main`.
- Local Git remote: `origin https://github.com/liviupop/uzduzuz26.git`.
- Project name: `uzduzuz26` (a Cloudflare Worker with the `[assets]` binding).
- Build command: `python3 regen.py` (regenerates `_index.json` on every deploy so an `_index.json` that's out of sync with the markdown can't sneak through).
- Build output directory: `/` (repo root — the site files are at the top level).
- Cloudflare URL: `https://uzduzuz26.uzinaduzina.workers.dev/`.

Current verification, 2026-05-06:

- GitHub CLI is authenticated as `liviupop` with push-capable repo access.
- Local `main` tracks `origin/main`; `gh repo view liviupop/uzduzuz26` resolves correctly.
- Cloudflare Worker URL responds with `HTTP 200`, `server: cloudflare`, and a `cf-ray` header, so the Cloudflare deployment itself is reachable.
- Custom domains `uzinaduzina.org` and `www.uzinaduzina.org` are attached to Worker `uzduzuz26` in the `Uzinaduzina@gmail.com` Cloudflare account.

Cloudflare's default `html_handling` strips `.html` extensions: a request for `/foo.html` returns a 307 to `/foo`, which then serves the contents of `foo.html`. This is fine for our diagram iframes (one extra round-trip) and works transparently for the SPA shell.

To deploy a change:

1. `python3 regen.py` (or skip — Cloudflare runs it during build anyway).
2. `git add -A && git commit -m "..."`.
3. `git push origin main`. Cloudflare auto-deploys in ~30–60 seconds.

---

## 15 · Domain and DNS

`uzinaduzina.org` registered at LuckyRegister (Wild West Domains / GoDaddy reseller). DNS authority delegated to Cloudflare:

```
Nameservers:
  greg.ns.cloudflare.com
  indie.ns.cloudflare.com
```

### Records preserved across the migration

When the zone moved to Cloudflare, the existing email infrastructure was carried over intact. On 2026-05-06, the final website cutover moved apex and `www` to the Worker while keeping mail on the old server:

| Record | Value | Proxy |
|---|---|---|
| Worker custom domain apex | `uzinaduzina.org` → Worker `uzduzuz26` | Proxied, Cloudflare-managed |
| Worker custom domain `www` | `www.uzinaduzina.org` → Worker `uzduzuz26` | Proxied, Cloudflare-managed |
| `MX` apex | priority 0 → `mail.uzinaduzina.org` | DNS only |
| `A mail` | `185.165.187.2` | DNS only |
| `A webmail`, `cpanel`, `webdisk`, `whm`, `autodiscover`, `autoconfig` | `185.165.187.2` | DNS only (mail subdomains must never be proxied — Cloudflare's proxy only handles HTTP/S) |
| `TXT` apex (SPF) | `v=spf1 ip4:185.165.187.2 +a +mx +ip4:45.153.91.2 +ip4:185.248.197.0 +ip4:185.165.185.101 +include:relay.mailbaby.net ~all` | DNS only |
| `TXT default._domainkey` (DKIM) | `v=DKIM1; k=rsa; p=...` | DNS only |
| `SRV _caldav._tcp`, `_caldavs._tcp`, `_carddav._tcp`, `_carddavs._tcp` | calendar/contact discovery | DNS only |
| `TXT _acme-challenge`, `_cpanel-dcv-test-record` | ACME and cPanel domain validation | DNS only |

### Cutover verification

Final cutover completed on 2026-05-06:

- Nameservers are correctly delegated to Cloudflare: `greg.ns.cloudflare.com` and `indie.ns.cloudflare.com`.
- Cloudflare generated proxied Worker DNS records for apex and `www` (`AAAA 100::` internally, flattened by Cloudflare to edge IPs for public resolvers).
- `https://uzinaduzina.org/` and `https://www.uzinaduzina.org/` return the static site through Cloudflare edge (`HTTP 200`, `server: cloudflare`) when resolved to the new Cloudflare IPs.
- Mail is isolated from the website cutover: `MX` points to `mail.uzinaduzina.org`, and `mail.uzinaduzina.org` points directly to `185.165.187.2`, DNS only.
- Some recursive resolvers may briefly serve the old `185.165.187.2` apex answer from cache. Authoritative Cloudflare DNS and public resolvers checked after cutover already returned Cloudflare edge IPs.

### Future hardening (not yet done)

- **DMARC**: add `v=DMARC1; p=none; rua=mailto:contact@uzinaduzina.org` for monitoring, then tighten to `quarantine` once confirmed clean.
- **MTA-STS** + **TLSRPT**: optional, modest mail-policy improvement.
- **CAA records**: pin which CAs can issue certs for the domain.

---

## 16 · Content inventory (current)

Tally as of last regen:

```
1   org           home, who-we-are, contact, projects, team, partners, manifesto
                  (mix of structured pages in notes.js and one .md file)
11  principles    sustainability through beauty, calm technologies,
                  education for the whole person, active citizenship as
                  a transversal practice, explicitly pro-European,
                  critical thinking, curiosity and experiment,
                  critical technology, living heritage, responsibility
                  toward the future, against disorientation
18  projects      Drops of Sustainable Development (lead, KA220-ADU, €250k, 2024-2026 ongoing)
                  Green Genesis Startup (lead, KA220-YOU, €205k, 2022-2024)
                  Goana după meteor (lead, AFCN, €36k, 2025)
                  Cabinet of retrofuturist curiosities (lead, Botnar, €8k, 2024-2025)
                  AI4NGOs (lead in Cluj for CCIF, August 2025)
                  Atât @ Artă.NonStop (lead, own, €420, 2024)
                  democraicy (concept, 2020, unfunded)
                  Greenycrafts (partner with 4 Elements, 2023-2024)
                  Wooden Wonders (beneficiary, 4 Elements, 2025, 9.08/10)
                  A Greener Future (beneficiary, CCIF, 2024)
                  Belgica 100 Workshops (beneficiary, Polish Institute, 2025)
                  Mini-grant FDSC (beneficiary, FDSC, 2020, €700)
                  Modeling the Future (beneficiary, CCIF, 2023)
                  Echoes of Unity (beneficiary, CCIF, 2024)
                  Do the Move (beneficiary, KA105, 2020-2021, Croatia)
                  Korczak Lens (partner, Polish Institute, 2021-2022)
                  SibiAR (lead, in development, Sibiu)
                  Polish Discoveries that Changed the World (partner, Polish Institute, March 2026)
7   team          Liviu Pop (president), Mihaela Bidilică-Vasilache,
                  Cosmina Timoce-Mocanu, Diana Spirleanu, Radu Pop,
                  Nectarie Pașca (youth needs · astronomy & astrophysics),
                  Siluan Pașca  (youth needs · piano & classical music)
1   partner full  CCIF Cyprus (more in the constellation, profiles pending)
2   curiosities   Dunbar, Alexander, and the dozen
                  The 2020 "Manifesto of City Dwellers Who Love the Village"
```

---

## 17 · The lineage of ideas

Some recurring threads worth knowing about:

### The dozen

The organisation's name (`uzinaduzina` ≈ "a dozen work initiatives in a factory") encodes a hypothesis about scale. Robin Dunbar's research identifies a *sympathy group* of 10–15 people — those one can hold in active memory by name and situation. Christopher Alexander's *A Pattern Language* (1977), Pattern 80, independently arrives at "5 to 20 workers" as the unit at which work can be self-governed by conversation rather than command. Two unrelated traditions converging on the same number. We work at this scale on purpose. The full argument lives in the `curiosity-dunbar-alexander-and-the-dozen` note.

### The critical-tech line

A continuous line of work on technology and democratic life since 2020:

- **2020** — *democraicy*: an early concept document on algorithms with civic conscience. Never funded, kept readable.
- **2024** — *Cabinet of retrofuturist curiosities*: Midjourney as a tool inside a school workshop, several years before that became unremarkable.
- **2025** — *AI4NGOs*: a youth-worker training on AI in civil society, hosted in Cluj for CCIF Cyprus.

The line is not flagged on the home page as a flagship; it's just there in the chronology if you read carefully.

### Living heritage

Folk astronomy at Mociu (the meteorite village, *Goana după meteor* 2025), traditional crafts in Croatia (*Wooden Wonders* 2025, *Greenycrafts* 2023-2024), the Sibiu multi-ethnic stories (*SibiAR*, in development), and the 2020 *Manifesto of City Dwellers Who Love the Village* archived as a curiosity. Heritage as material to work with, not a relic in a display case.

### Pro-European structurally

Eight Erasmus+ projects, two coordinated as lead (Green Genesis €205k, Drops €250k). Partners across Cyprus (CCIF, our strongest tie at six projects), Italy (CO-LABORY, Petit Pas), Greece (Innovation Hive), Spain (ACD La Hoya), Croatia (Udruga Murtila, 4 Elements), Poland (Polish Institute, DIP). Being European is funding architecture, not flavour.

---

## 18 · What this site does NOT do

A short manifest of refusals — useful when adding new pages or features:

- No emoji, no decorative SVGs, no stock photography, no hero sections.
- No carousels, popups, modal dialogs, autoplay video.
- No gradients (in the modern web sense), no neumorphism, no glassmorphism, no frosted-glass anything.
- No "trusted by" logo strips.
- No drop shadows. The only "elevation" is a 1px rule.
- No login walls, no paywalls, no intrusive cookie banners (we do not set tracking cookies).
- No recommendation algorithm, no engagement metrics, no analytics-driven layout decisions.
- No JavaScript framework (React/Vue/Svelte). The interaction layer is hand-written in ~1000 lines of vanilla JS.
- No CSS framework (Tailwind/Bootstrap). Tokens + components in ~1500 lines of vanilla CSS.
- No build step in the editor's loop (`regen.py` is one file, no deps, runs in <1 s).

---

## 19 · Open / not-yet-done

Things known to be incomplete, in rough priority order:

1. **Apex/www → Worker cutover.** See section 15. The new site is fully ready; flipping the apex A record is one click in Cloudflare.
2. **More partner profiles.** CCIF Cyprus has a full note. The other ten partners (4 Elements, CO-LABORY, Innovation Hive, ACD La Hoya, Udruga Murtila, Polish Institute, Petit Pas, DIP, CCCluj, Lucian Rafan-Szekely) are in the constellation but lack their own notes.
3. **Develop-association kindergarten exhibition.** A 2026 project with concept and photos to be added later.
4. **Project images.** Most project notes are text-only. The `assets/images/` folder is ready; image references are supported. Drop in photos when available.
5. **DMARC and MTA-STS** for email auth hardening. Safe to add now that the migration is stable.
6. **EN/RO toggle?** The site is currently English-only. Romanian sources exist for many notes (in the parent directory of this repo) and could be translated back if a bilingual site is wanted later.
7. **A `/jurnal` or notebook section** for short reflective posts — present in the original PRD but not yet implemented.
8. **Comprehensive RSS** (`/feed.xml` per the PRD) — currently only a sitemap.
9. **JSON-LD structured data** per page. Currently there's `llms.txt` but no schema.org JSON-LD. AI crawlers tolerate the front matter as-is, but search engines would benefit.

---

## 20 · Agent discoverability

The audience priority states "AI crawlers first" (see §1). Beyond `llms.txt`, `robots.txt`, and serving Markdown source alongside HTML, the site publishes a small set of standardised discovery files so agents can find structure without scraping.

### What is published

| Endpoint | Spec | Purpose |
|---|---|---|
| `/robots.txt` | de-facto + draft Content Signals | crawler permissions; `Content-Signal: ai-train=yes, search=yes, ai-input=yes` |
| `/llms.txt` | https://llmstxt.org/ | flat overview optimised for LLMs |
| `/.well-known/api-catalog` | RFC 9727 | linkset listing the content endpoints, docs, and the `_index.json` "service description" |
| `/.well-known/agent-skills/index.json` | Cloudflare Agent Skills RFC v0.2 | discovery index for the four agent skills the site exposes (with sha256 digests) |
| `/.well-known/agent-skills/*.md` | (linked from index.json) | one description file per skill: `browse-notes`, `list-notes`, `fetch-note-markdown`, `open-note-stack` |
| `/.well-known/mcp/server-card.json` | MCP draft (SEP-1649) | server card pointing at the live MCP endpoint at `/mcp` |
| `/mcp` | MCP 2025-06-18 streamable-HTTP transport | live JSON-RPC 2.0 server exposing `list_notes` and `fetch_note_markdown` as proper MCP `tools/call` methods |
| `/.well-known/oauth-protected-resource` | RFC 9728 | Protected Resource Metadata declaring the site as public (`bearer_methods_supported: []`, `scopes_supported: []`, `authorization_servers: []`) |
| `/.well-known/oauth-authorization-server` | RFC 8414 | minimal Authorization Server Metadata with empty `grant_types_supported`, `response_types_supported`, `scopes_supported`. Honest no-op: declares "no OAuth flows are available here" |
| `/.well-known/openid-configuration` | OIDC Discovery 1.0 | minimal OIDC metadata. Required URL fields point at `/.well-known/oauth-disabled` (HTTP 410). Same honest no-op pattern |
| `/.well-known/oauth-disabled` | site-internal | Worker-served HTTP 410 Gone with structured JSON error. Any agent that follows an OIDC endpoint URL lands here and gets a clear "no auth server" response |
| Markdown content negotiation | de-facto + https://developers.cloudflare.com/fundamentals/reference/markdown-for-agents/ | requests to `/` or `/?n=<slug>` with `Accept: text/markdown` get the markdown source instead of the HTML shell |
| `/sitemap.xml` | sitemap.org | canonical URL index for traditional crawlers |
| `<link>` tags in `index.html` | RFC 8288 link relations | same discovery URLs re-stated in HTML so agents fetching only the homepage can find them without parsing response headers |
| `Link:` response headers | RFC 8288 | added by the Cloudflare Worker shim (`src/worker.js`) on every HTML response, so agents that look at HTTP headers find the discovery endpoints without parsing the body |
| `_headers` file | Cloudflare Pages convention | mirrors the same headers; picked up if you redeploy via Pages |
| WebMCP tools (in `app.js`) | https://webmachinelearning.github.io/webmcp/ | three read-only tools exposed to in-browser agents: `list_notes`, `fetch_note_markdown`, `open_note_stack` |

### How the Worker shim works

The site is deployed as a Cloudflare Worker with a static-assets binding. `wrangler.jsonc` declares:

```jsonc
{
  "name": "uzduzuz26",
  "main": "src/worker.js",
  "compatibility_date": "2026-05-07",
  "assets": {
    "directory": ".",
    "binding": "ASSETS",
    "run_worker_first": true
  }
}
```

`run_worker_first: true` makes the Worker run on every request. The Worker (`src/worker.js`, ~80 lines) calls `env.ASSETS.fetch(request)` to get the static asset, then post-processes the response:

- For HTML, appends seven `Link:` headers (api-catalog, agent-skills, two service-doc, describedby, alternate, sitemap) and `Vary: Accept`.
- For `.md` paths, sets `Content-Type: text/markdown; charset=utf-8`.
- For `/.well-known/api-catalog`, sets `Content-Type: application/linkset+json; charset=utf-8`.
- For `/.well-known/agent-skills/index.json`, sets `Content-Type: application/json; charset=utf-8`.
- For all responses, sets `Referrer-Policy: strict-origin-when-cross-origin` and `X-Content-Type-Options: nosniff` if not already present.

The Worker is intentionally minimal: no state, no third-party calls, no rewriting of bodies. If the Worker fails to deploy, the static assets continue serving (without the extra headers) — the site does not go dark.

### Agent Skills index

The four published skills:

| Name | Type | What it does |
|---|---|---|
| `browse-notes` | navigation | Construct a `?n=slug,slug,slug` URL that opens a stacked-notes view |
| `list-notes` | data-fetch | `GET /content/_index.json` — every published note's metadata in one JSON array |
| `fetch-note-markdown` | data-fetch | `GET /content/<slug>.md` — the original markdown source of a single note |
| `open-note-stack` | webmcp-tool | In-browser only: programmatically open a stack via `navigator.modelContext.provideContext()` |

Each skill has a markdown description at `/.well-known/agent-skills/<name>.md` with the full URL grammar, examples, and edge cases. The index pins each description by sha256 so an agent can verify the file content matches what it expects.

When a skill description changes, recompute the digest and update the index:

```sh
cd .well-known/agent-skills
shasum -a 256 *.md     # paste the new hashes into index.json
```

### MCP server (live)

The site runs a real Model Context Protocol server, embedded in the same Cloudflare Worker as the asset shim. Source: `src/mcp.js` (~250 lines, no dependencies). Spec: https://modelcontextprotocol.io/specification/2025-06-18

- **Transport:** streamable-HTTP. JSON-RPC 2.0 over POST. No SSE, no batched requests, no session IDs (the server is stateless and read-only).
- **Endpoint:** `https://uzinaduzina.org/mcp`
- **Server card:** `https://uzinaduzina.org/.well-known/mcp/server-card.json`
- **GET to /mcp:** returns a brief human-readable summary of the server (helpful for `curl` debugging); JSON-RPC clients always POST.
- **CORS:** open (`Access-Control-Allow-Origin: *`) so any origin can connect.
- **Auth:** none required.

#### Tools

| MCP method | Action |
|---|---|
| `initialize` | returns `serverInfo`, `protocolVersion: "2025-06-18"`, `capabilities: { tools: { listChanged: false } }`, and human-readable instructions |
| `tools/list` | returns the two tools below with their JSON Schemas |
| `tools/call` with `name: "list_notes"` | returns the full content of `/content/_index.json` as `text` content + `structuredContent.notes` |
| `tools/call` with `name: "fetch_note_markdown"`, `arguments: { slug: "..." }` | returns the markdown of a single note as `text` content + `structuredContent.{slug, markdown}` |
| `ping` | returns `{}` |
| `notifications/initialized` etc. | accepted with HTTP 202 |

#### Quick test

```bash
# initialize
curl -sS https://uzinaduzina.org/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | jq

# list tools
curl -sS https://uzinaduzina.org/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | jq

# call list_notes
curl -sS https://uzinaduzina.org/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_notes","arguments":{}}}' | jq

# call fetch_note_markdown
curl -sS https://uzinaduzina.org/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"fetch_note_markdown","arguments":{"slug":"manifesto-living-heritage"}}}' | jq
```

The MCP tools internally call `env.ASSETS.fetch()` to read static files; they cannot reach anything beyond what is already served as public content.

### OAuth/OIDC discovery as honest no-op

We are not an OAuth or OpenID Connect authorization server. We do publish the discovery documents anyway, because the agent-readiness audit checks for their existence and the absence flagged false positives. The published documents take care to *signal* the absence of any auth flow rather than pretend one exists:

- **`/.well-known/oauth-authorization-server` (RFC 8414):** all flow arrays are empty (`grant_types_supported: []`, `response_types_supported: []`, `scopes_supported: []`). The `_note` field in the body explains in prose. No `authorization_endpoint` or `token_endpoint` is published — those are required only when matching grant types are supported, and ours has none.
- **`/.well-known/openid-configuration` (OIDC Discovery 1.0):** required URL fields are present (the spec requires them) but all of them point at `/.well-known/oauth-disabled`, which the Worker serves as **HTTP 410 Gone** with this body:
  ```json
  {
    "error": "oauth_disabled",
    "error_description": "uzinaduzina.org does not run an OAuth 2.0 or OpenID Connect server. There are no token, authorization, userinfo, jwks, or registration endpoints. The site is public and unauthenticated.",
    "error_uri": "https://uzinaduzina.org/DOCUMENTATION.md",
    "canonical_resource_metadata": "https://uzinaduzina.org/.well-known/oauth-protected-resource"
  }
  ```
  Any agent that actually tries to start an OIDC flow lands on this URL and receives a clear, structured failure instead of a silent or misleading response.

The canonical truth — "this is a public resource that takes no tokens" — lives in `/.well-known/oauth-protected-resource` (RFC 9728). The OAuth/OIDC discovery documents above point at it via `service_documentation` and `_note` fields.

If a future feature requires actual auth (a private contact form, a partner-only dashboard, a write-API beyond MCP), replace these no-ops with real metadata pointing at a real authorization server.

### Markdown content negotiation (live)

The Worker now does its own content negotiation, so we don't need Cloudflare's "Markdown for Agents" toggle (it works regardless of whether that feature is enabled for the account).

When a request to `/` or `/?n=<slug>` arrives with `Accept: text/markdown` (or `application/markdown`), the Worker:

1. Picks the active slug from `?n=` and `?a=` (defaulting to the rightmost slug).
2. Fetches `/content/<slug>.md` from the assets binding.
3. Returns it with `Content-Type: text/markdown; charset=utf-8`, `Vary: Accept`, an `X-Markdown-Tokens` estimate (~chars/4 heuristic), and `X-Markdown-Source: /content/<slug>.md`.
4. If no slug is named, or the named slug has no markdown counterpart, falls back to `/llms.txt`.

Browsers (which send `Accept: text/html, ...`) get the normal HTML shell unchanged. The negotiation is opt-in.

```bash
# Default: HTML
curl -sI https://uzinaduzina.org/ | grep -i content-type
# → content-type: text/html; charset=utf-8

# Opt in: markdown
curl -sI -H 'Accept: text/markdown' https://uzinaduzina.org/?n=manifesto-living-heritage | grep -iE '^(content-type|x-markdown|vary)'
# → content-type: text/markdown; charset=utf-8
# → vary: Accept
# → x-markdown-tokens: 1234
# → x-markdown-source: /content/manifesto-living-heritage.md
```

### OAuth Protected Resource Metadata

`/.well-known/oauth-protected-resource` is published as a minimal RFC 9728 document that explicitly declares the site as **public**:

```json
{
  "resource": "https://uzinaduzina.org/",
  "resource_name": "uzinaduzina.org content",
  "resource_documentation": "https://uzinaduzina.org/DOCUMENTATION.md",
  "bearer_methods_supported": [],
  "scopes_supported": [],
  "authorization_servers": []
}
```

The empty arrays are the meaningful payload: `bearer_methods_supported: []` says "we accept no bearer tokens"; `authorization_servers: []` says "no servers issue tokens for this resource"; `scopes_supported: []` says "no permission scopes defined". An agent reading this document understands: this resource is public, no token is required, no auth flow exists.

This is honest and is what the audit is checking for. It does not pretend to be an authorization server.

---

## 21 · Quick reference: working with the site

**Add a project note:**
```sh
cat > content/project-foo.md <<EOF
---
title: "Foo"
slug: project-foo
type: project
kicker: "project · 2026 · lead"
accent: indigo
role: lead
order: 19
summary: "What it is, in one line."
pills: ["ongoing|on", "2026"]
---

The first paragraph becomes the lede.

## what it was

Body...
EOF
python3 regen.py
git add content/project-foo.md content/_index.json
git commit -m "Add project Foo"
git push
```

**Edit a manifesto principle:** edit `content/manifesto-<slug>.md`, no regen needed if only the body changed (regen if you changed `title`, `kicker`, `summary`, `order`, etc.).

**Tweak the home page or an index:** edit `notes.js` directly. No regen needed.

**Add a partner to the constellation:** edit `assets/data.js`, append to `projects:` and/or `partners:`.

**Add a custom diagram:** copy any `assets/diagrams/*.html` as a template, edit, then iframe it from the relevant markdown note.

**Test locally:**
```sh
cd uzduz25 && python3 -m http.server 8080
open http://localhost:8080
```

**Deploy:** push to `main`. Cloudflare runs `regen.py` and ships in under a minute.

---

*Last updated when committed. The repo's git log is authoritative for what changed when.*
