diff --git a/modules/rflows/components/flows.css b/modules/rflows/components/flows.css
index b247013..e8cf767 100644
--- a/modules/rflows/components/flows.css
+++ b/modules/rflows/components/flows.css
@@ -71,6 +71,120 @@
--rflows-modal-border: #334155;
}
+/* ── Organic / Mycorrhizal mode overrides ────────────── */
+:host([data-render-mode="organic"]) {
+ /* Source node */
+ --rflows-source-bg: #365314;
+ --rflows-source-border: #84cc16;
+ --rflows-source-rate: #a3e635;
+
+ /* Edge colors — earth tones */
+ --rflows-edge-inflow: #84cc16;
+ --rflows-edge-spending: #fbbf24;
+ --rflows-edge-overflow: #a3e635;
+
+ /* Funnel zones */
+ --rflows-zone-drain: #7f1d1d;
+ --rflows-zone-drain-opacity: 0.06;
+ --rflows-zone-healthy: #365314;
+ --rflows-zone-healthy-opacity: 0.06;
+ --rflows-zone-overflow: #a16207;
+ --rflows-zone-overflow-opacity: 0.05;
+ --rflows-fill-opacity: 0.35;
+
+ /* Funnel labels */
+ --rflows-label-inflow: #84cc16;
+ --rflows-label-spending: #fbbf24;
+ --rflows-label-overflow: #a3e635;
+
+ /* Status colors — organic palette */
+ --rflows-status-critical: #b91c1c;
+ --rflows-status-sustained: #a16207;
+ --rflows-status-overflow: #65a30d;
+ --rflows-status-thriving: #65a30d;
+ --rflows-sat-bar: #84cc16;
+ --rflows-sat-border: #d97706;
+
+ /* Outcome / progress */
+ --rflows-status-completed: #65a30d;
+ --rflows-status-blocked: #b91c1c;
+ --rflows-status-inprogress: #a16207;
+ --rflows-status-notstarted: #78716c;
+ --rflows-phase-unlocked: #84cc16;
+
+ /* Score badge */
+ --rflows-score-gold: #d97706;
+ --rflows-score-green: #65a30d;
+ --rflows-score-amber: #a16207;
+ --rflows-score-red: #b91c1c;
+
+ /* Card value */
+ --rflows-card-value: #d97706;
+
+ /* Selection */
+ --rflows-selected: #84cc16;
+
+ /* Inline edit buttons */
+ --rflows-btn-done: #65a30d;
+ --rflows-btn-delete: #b91c1c;
+ --rflows-btn-fund: #84cc16;
+ --rflows-btn-save: #65a30d;
+
+ /* Sufficiency tooltip highlight */
+ --rflows-sufficiency-highlight: #d97706;
+
+ /* Edge drag handle */
+ --rflows-drag-handle-fill: #365314;
+ --rflows-drag-handle-stroke: #4d7c0f;
+
+ /* Modal border accent */
+ --rflows-modal-border: #365314;
+}
+
+/* Organic canvas background */
+:host([data-render-mode="organic"]) .flows-canvas-svg {
+ background-color: #0f1a0f;
+ background-image: none;
+}
+
+/* Organic port styling */
+:host([data-render-mode="organic"]) .port-dot {
+ r: 8;
+ stroke: #365314;
+ stroke-width: 2.5;
+ filter: drop-shadow(0 0 3px currentColor);
+}
+:host([data-render-mode="organic"]) .port-group:hover .port-dot {
+ r: 10;
+ filter: drop-shadow(0 0 6px currentColor);
+}
+
+/* Organic edge animation — sparse dot pattern, slower */
+:host([data-render-mode="organic"]) .org-hypha-path {
+ stroke-dasharray: 3 8;
+ animation: organicFlow 2.5s linear infinite;
+}
+@keyframes organicFlow { to { stroke-dashoffset: -22; } }
+
+/* Organic overflow bud pulse */
+:host([data-render-mode="organic"]) .org-overflow-bud--active {
+ animation: budPulse 2s ease-in-out infinite;
+}
+@keyframes budPulse {
+ 0%, 100% { ry: 13; opacity: 0.55; }
+ 50% { ry: 16; opacity: 0.75; }
+}
+
+/* Organic spore terminus glow */
+:host([data-render-mode="organic"]) .org-spore-terminus {
+ filter: drop-shadow(0 0 3px currentColor);
+}
+
+/* Organic legend dots */
+:host([data-render-mode="organic"]) .flows-canvas-legend-dot {
+ border-radius: 50%;
+}
+
/* ── Base ────────────────────────────────────────────── */
.flows-landing, .flows-detail {
font-family: system-ui, -apple-system, sans-serif;
diff --git a/modules/rflows/components/folk-flows-app.ts b/modules/rflows/components/folk-flows-app.ts
index 3a072dd..a66a109 100644
--- a/modules/rflows/components/folk-flows-app.ts
+++ b/modules/rflows/components/folk-flows-app.ts
@@ -20,6 +20,7 @@ import { mapFlowToNodes } from "../lib/map-flow";
import { flowsSchema, flowsDocId, type FlowsDoc, type CanvasFlow } from "../schemas";
import type { DocumentId } from "../../../shared/local-first/document";
import { FlowsLocalFirstClient } from "../local-first-client";
+import { OrganicRenderer, organicSvgDefs, type OrganicRendererContext } from "./folk-flows-organic-renderer";
interface FlowSummary {
id: string;
@@ -197,6 +198,28 @@ class FolkFlowsApp extends HTMLElement {
// Tour engine
private _tour!: TourEngine;
+
+ // Render mode: mechanical (default) or organic (mycorrhizal)
+ private renderMode: "mechanical" | "organic" = "mechanical";
+ private _organicRenderer: OrganicRenderer | null = null;
+ private get organicRenderer(): OrganicRenderer {
+ if (!this._organicRenderer) {
+ const ctx: OrganicRendererContext = {
+ getNodeSize: (n) => this.getNodeSize(n),
+ vesselWallInset: (yFrac, taper) => this.vesselWallInset(yFrac, taper),
+ computeVesselFillPath: (w, h, fill, taper) => this.computeVesselFillPath(w, h, fill, taper),
+ renderPortsSvg: (n) => this.renderPortsSvg(n),
+ renderSplitControl: (nid, at, allocs, cx, cy, tw) => this.renderSplitControl(nid, at, allocs, cx, cy, tw),
+ formatDollar: (a) => this.formatDollar(a),
+ esc: (s) => this.esc(s),
+ _currentFlowWidths: this._currentFlowWidths,
+ };
+ this._organicRenderer = new OrganicRenderer(ctx);
+ }
+ // Keep flow widths reference current
+ (this._organicRenderer as any).ctx._currentFlowWidths = this._currentFlowWidths;
+ return this._organicRenderer;
+ }
private static readonly TOUR_STEPS = [
{ target: '[data-canvas-action="add-source"]', title: "Add a Source", message: "Sources represent inflows of resources. Click the + Source button to add one.", advanceOnClick: true },
{ target: '[data-canvas-action="add-funnel"]', title: "Add a Funnel", message: "Funnels allocate resources between spending and overflow. Click + Funnel to add one.", advanceOnClick: true },
@@ -227,6 +250,11 @@ class FolkFlowsApp extends HTMLElement {
new MutationObserver(() => this._syncTheme())
.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
+ // Restore render mode preference
+ const savedMode = localStorage.getItem("rflows:render-mode");
+ if (savedMode === "organic" || savedMode === "mechanical") this.renderMode = savedMode;
+ if (this.renderMode === "organic") this.setAttribute("data-render-mode", "organic");
+
// Read view attribute, default to canvas (detail) view
const viewAttr = this.getAttribute("view");
this.view = viewAttr === "budgets" ? "budgets" : viewAttr === "mortgage" ? "mortgage" : "detail";
@@ -994,6 +1022,8 @@ class FolkFlowsApp extends HTMLElement {
+
+