diff --git a/hubzilla/addon/vs01/VS-RENDERER-SPEC.md b/hubzilla/addon/vs01/VS-RENDERER-SPEC.md
new file mode 100644
index 0000000..141ec68
--- /dev/null
+++ b/hubzilla/addon/vs01/VS-RENDERER-SPEC.md
@@ -0,0 +1,516 @@
+# VS Renderer Specification
+
+**Project:** kane-diagnostics
+**Version:** 1.0
+**Applies to:** `hubzilla/addon/vs01/vs01.php` and any future addon that renders VS schema forms
+**Prerequisite reading:** All ten VS schema files in `hubzilla/addon/vs01/vital-signs/`
+
+---
+
+## Purpose
+
+This document specifies how the PHP renderer reads the VS JSON schemas and produces the form interface. The renderer knows nothing about what a Vital Sign is. It only knows how to render what the schema describes. All diagnostic logic, field structure, flag conditions, and display instructions live in the JSON. The renderer's job is to execute those instructions faithfully.
+
+This is the contract between the schema and the code. When the schema changes, the renderer does not change — unless a new field type or behavior is introduced that the renderer does not yet handle. In that case this document is revised first, then the renderer is updated.
+
+---
+
+## Schema Loading
+
+The renderer loads schemas from `addon/vs01/vital-signs/`. Files are named `VS-{NN}-{slug}.json`. The renderer loads all files in this directory on each request. File order is determined by the numeric prefix — VS-01 before VS-02, and so on.
+
+```php
+function vs01_load_schemas() {
+ $dir = 'addon/vs01/vital-signs/';
+ $files = glob($dir . 'VS-*.json');
+ if (!$files) return [];
+ sort($files);
+ $schemas = [];
+ foreach ($files as $file) {
+ $raw = @file_get_contents($file);
+ if (!$raw) continue;
+ $data = json_decode($raw, true);
+ if (json_last_error() !== JSON_ERROR_NONE) continue;
+ $code = $data['_meta']['code'] ?? null;
+ if ($code) $schemas[$code] = $data;
+ }
+ return $schemas;
+}
+```
+
+If a schema file cannot be read or parsed, it is skipped and a visible error is logged. The renderer never silently ignores a missing or malformed schema.
+
+---
+
+## Access State and Perspective Routing
+
+Each schema defines three perspective slots: `homeowner`, `professional`, `public_record`. Each slot carries an `access_state` value. The renderer maps the current visitor's access state to the correct perspective slot.
+
+| Access state | Perspective rendered |
+|---|---|
+| `public` | No form — access wall only |
+| `participant` | `homeowner` perspective |
+| `professional` | `professional` perspective |
+| `operator` | All three perspectives — read-only view of homeowner and professional, editable public_record |
+
+The `professional` access state corresponds to a Tier-I or Tier-II listed professional whose entry in `listings.json` has `status: active`. The renderer calls `vs01_access_state()` and routes accordingly. A `professional` visitor sees the homeowner record as read-only and their own professional slot as editable.
+
+The operator sees all three slots. The homeowner and professional slots are read-only in the operator view. The public_record slot is editable only if `immutable_after_submit` is false or no public record submission exists yet for this VS code and association.
+
+---
+
+## Form Structure
+
+Each VS renders as a single form. The form contains:
+
+1. A VS header — code, title, diagnostic question from `_meta`
+2. A verification sources panel — expandable, lists each source with label, URL, and instruction
+3. The perspective slot — the fields for the current visitor's access state
+4. A submit button
+5. A CSRF token field
+
+The form action posts to `/vs01` with hidden fields for `vs_code` and `perspective`.
+
+---
+
+## Field Types
+
+The renderer implements one function per field type. All seven types are defined here. No other field type may appear in a schema without a corresponding entry in this spec and a corresponding renderer function.
+
+### `text`
+
+Single-line text input.
+
+```php
+function vs01_render_field_text($field, $value = '') {
+ $id = vs01_h($field['id']);
+ $label = vs01_h($field['label']);
+ $ph = vs01_h($field['placeholder'] ?? '');
+ $req = !empty($field['required']) ? 'required' : '';
+ return '
+
+ ';
+}
+```
+
+### `multiselect`
+
+Checkbox group. Options come from `$field['options']`. Stored as a JSON-encoded array of selected values. Introduced in VS-08 for `pro_dsc_cascade`.
+
+```php
+function vs01_render_field_multiselect($field, $value = '') {
+ $id = vs01_h($field['id']);
+ $label = vs01_h($field['label']);
+ $options = $field['options'] ?? [];
+ $selected = $value ? json_decode($value, true) : [];
+ if (!is_array($selected)) $selected = [];
+ $out = '
';
+ $out .= '
';
+ return $out;
+}
+```
+
+---
+
+## Field Dispatcher
+
+A single function dispatches to the correct type renderer.
+
+```php
+function vs01_render_field($field, $submitted_values = []) {
+ $id = $field['id'] ?? '';
+ $type = $field['type'] ?? 'text';
+ $value = $submitted_values[$id] ?? '';
+
+ $html = '';
+ switch ($type) {
+ case 'text': $html = vs01_render_field_text($field, $value); break;
+ case 'number': $html = vs01_render_field_number($field, $value); break;
+ case 'date': $html = vs01_render_field_date($field, $value); break;
+ case 'boolean': $html = vs01_render_field_boolean($field, $value); break;
+ case 'select': $html = vs01_render_field_select($field, $value); break;
+ case 'textarea': $html = vs01_render_field_textarea($field, $value); break;
+ case 'multiselect': $html = vs01_render_field_multiselect($field, $value); break;
+ default:
+ $html = '
Unknown field type: '
+ . vs01_h($type) . '
';
+ }
+
+ $html .= vs01_render_diagnostic_flag($field, $value);
+ return $html;
+}
+```
+
+---
+
+## Conditional Display — `depends_on`
+
+A field with a `depends_on` property is hidden until the condition is met. The renderer outputs the field wrapped in a container with a `data-depends-on` attribute. JavaScript handles show/hide. PHP always renders the field — JavaScript controls visibility.
+
+```php
+function vs01_wrap_depends_on($field, $html) {
+ if (empty($field['depends_on'])) return $html;
+ $dep_field = vs01_h($field['depends_on']['field']);
+ $dep_value = $field['depends_on']['value'];
+ if (is_array($dep_value)) {
+ $dep_value = vs01_h(json_encode($dep_value));
+ } else {
+ $dep_value = vs01_h((string) $dep_value);
+ }
+ return '
+
+ ' . $html . '
+
+ ';
+}
+```
+
+The JavaScript in `vs01.js` reads `data-depends-on` and `data-depends-value` on page load and on every change event, showing or hiding the container accordingly.
+
+---
+
+## Diagnostic Flags
+
+A field with `diagnostic_flag: true` renders a flag panel immediately after the field when the flag condition is met. The renderer evaluates the flag condition against the current submitted value. On initial load with no submitted value, no flag is shown. After submission, flags are evaluated server-side and rendered with the form.
+
+```php
+function vs01_render_diagnostic_flag($field, $value) {
+ if (empty($field['diagnostic_flag'])) return '';
+ $condition = $field['flag_condition'] ?? null;
+ if ($condition === null) return '';
+
+ $triggered = false;
+ if (is_array($condition)) {
+ $triggered = in_array($value, $condition);
+ } elseif (is_bool($condition)) {
+ $triggered = ($value === ($condition ? 'true' : 'false'));
+ } else {
+ $triggered = ($value === (string) $condition);
+ }
+
+ if (!$triggered) return '';
+
+ $label = vs01_h($field['flag_label'] ?? 'Diagnostic condition');
+ $note = isset($field['flag_note'])
+ ? '
+ ';
+}
+```
+
+### Flag Severity
+
+Three severity levels control visual prominence. The schema does not carry an explicit severity field — severity is derived from the field id prefix and VS code.
+
+```php
+function vs01_flag_severity($field) {
+ $id = $field['id'] ?? '';
+ // Self-dealing loop — VS-03 pro_selfdealing_verified
+ if ($id === 'pro_selfdealing_verified') return 'critical';
+ // Dissolution cascade — VS-08 pro_standing_verified
+ if ($id === 'pro_standing_verified') return 'critical';
+ // Compound condition fields — VS-06 pro_incumbent_perpetuation_risk
+ if ($id === 'pro_incumbent_perpetuation_risk') return 'critical';
+ // Professional verification flags default to high
+ if (str_starts_with($id, 'pro_')) return 'high';
+ // Homeowner observation flags default to standard
+ return 'standard';
+}
+```
+
+CSS classes: `vs01-flag-critical`, `vs01-flag-high`, `vs01-flag-standard`. Visual treatment defined in `vs01.css`.
+
+---
+
+## Compound Condition Detection
+
+Three compound conditions span multiple VS codes. They are evaluated when the operator views the full VS record for an association. They are not evaluated per-field or per-VS — they are evaluated once across the complete VS record set.
+
+### Incumbent Perpetuation Triad
+
+Triggered when all three conditions are present in the same association record:
+
+- VS-02: `ho_quorum_achieved` = `yes_undocumented` OR `pro_quorum_structurally_achievable` = `false`
+- VS-05: `pro_enforcement_of_void_provision` = `true`
+- VS-06: `pro_incumbent_perpetuation_risk` = `true`
+
+Display: a banner above the VS navigation reading — *"Incumbent perpetuation triad present — quorum failure, outdated election procedures, and absence of qualification requirements confirmed for this association."*
+
+### Dissolution Cascade
+
+Triggered when VS-08 `pro_standing_verified` is `dissolved_administrative`, `dissolved_voluntary`, or `not_found`.
+
+Display: a banner listing the affected DSC categories — DSC-01, DSC-02, DSC-04, DSC-10 — with the note: *"Association dissolved — all board authority, assessment authority, lien authority, and foreclosure authority affected."*
+
+### Financial Unverifiability
+
+Triggered when VS-09 `pro_audit_verified` is `none` AND any of the following are present in the same record:
+
+- VS-04: `pro_ledger_reconcilable` = `no` or `records_unavailable`
+- VS-04: `pro_attorney_fees_authorized` = `no`
+- VS-09: `pro_financial_records_reconcilable` = `no` or `not_produced`
+
+Display: a note reading — *"Financial claims in this record cannot be independently verified. No independent audit exists and financial records are incomplete or unproduced."*
+
+---
+
+## Cross-VS Reference Notes
+
+Two VS schema fields explicitly reference another VS code:
+
+- VS-05 flag on unrecorded amendments: *"See VS-10 for the full Declaration recording currency analysis."*
+- VS-10 `pro_vs05_cross_reference`: links to the VS-05 record for this association.
+- VS-06 flag on Bylaws not requiring certification: *"See VS-03 for the full manager certification analysis."*
+
+The renderer outputs these as linked callouts when the referenced VS code exists in the loaded schema set.
+
+```php
+function vs01_render_cross_reference($vs_code, $label) {
+ return '
+
+ ';
+}
+```
+
+---
+
+## Immutable Public Record
+
+The `public_record` perspective slot carries `immutable_after_submit: true`. Once a public record submission exists for a VS code and association, the renderer outputs the public record slot as a read-only display, not a form. No submit button is shown. A note reads: *"Public record — immutable. Contact the operator to correct spelling or citation errors."*
+
+The renderer checks for an existing public record submission before rendering the public record perspective:
+
+```php
+function vs01_public_record_exists($vs_code, $association_id) {
+ // TODO: query orchestrator or local TMP store for existing public record
+ // Returns true if a submitted and promoted public record exists
+ return false;
+}
+```
+
+---
+
+## Verification Sources Panel
+
+Each schema `_meta` block carries a `verification_sources` array. The renderer outputs this as a collapsible panel above the form fields.
+
+```php
+function vs01_render_verification_sources($meta) {
+ $sources = $meta['verification_sources'] ?? [];
+ if (empty($sources)) return '';
+ $out = '
';
+ return $out;
+}
+```
+
+---
+
+## Information Record Distinction
+
+VS-07 and VS-09 homeowner perspectives are information records — the homeowner records what was communicated, not what they verified. The renderer signals this distinction with a note above the homeowner fields for these two VS codes, drawn from the schema `_meta.renderer_notes` field.
+
+```php
+function vs01_render_perspective_note($schema, $perspective_key) {
+ $notes = $schema['_meta']['renderer_notes'] ?? '';
+ if (!$notes) return '';
+ // Only render the note for homeowner perspective on VS-07 and VS-09
+ $code = $schema['_meta']['code'] ?? '';
+ if ($perspective_key !== 'homeowner') return '';
+ if (!in_array($code, ['VS-07', 'VS-09'])) return '';
+ return '
' . vs01_h($notes) . '
';
+}
+```
+
+---
+
+## Spool POST Handler
+
+On form submission the renderer validates required fields, assembles the spool envelope, and POSTs to the orchestrator receiver. The envelope follows the contract defined in each schema's `spool` block.
+
+```php
+function vs01_build_spool_envelope($vs_code, $perspective, $fields, $association_address) {
+ return [
+ 'vs_code' => $vs_code,
+ 'perspective' => $perspective,
+ 'association_address' => $association_address,
+ 'submitted_at' => date('c'),
+ 'standing' => vs01_access_state(),
+ 'fields' => $fields,
+ ];
+}
+```
+
+The envelope is JSON-encoded and POSTed with the node token header to the receiver URL from `config.json`. On success the renderer shows a confirmation. On failure it shows a visible error — never silent failure.
+
+---
+
+## File Size Constraint
+
+The renderer functions defined in this spec, when implemented in `vs01.php`, must not cause the file to exceed 500 lines. If the combined function set approaches that limit, extract the renderer functions into `vs01_renderer.php` as a required file. The split point is a single `require_once` in `vs01.php` — the addon entry point remains clean.
+
+```php
+// In vs01.php if extraction is needed:
+require_once 'addon/vs01/vs01_renderer.php';
+```
+
+This extraction does not require a spec revision — it is an implementation decision the developer makes when the line count requires it.