# FLUX — Full System Reference

This document covers every Flux-related component in the `render` repository: the mathematical foundations, the two model versions, the find-to-drive pipeline, the render engines, the experiment infrastructure, the frontend surfaces, and the measured properties that make the system work.

---

## 1. Core Thesis

A generative model is deterministic transport. The seed is the only authorable input. FLUX (Black Forest Labs' family of flow-matching diffusion models) has two properties that make it uniquely suited to authored motion:

1. **Bit-deterministic**: same seed produces `|delta| = 0.00000`. The seed is a real input, not a die.
2. **Low seed-to-layout coupling**: FLUX.1 2.82%, FLUX.2 3.72% (vs SDXL ~50%). Small perturbations in the initial latent change *detail*, not *composition*. This means an authored trajectory through latent space produces coherent motion at the composition scale.

The **moment operator** -- an exact angular coordinate theta -- drives a structured, closed path through FLUX's initial latent. FLUX transports that authored geometry into a coherent image sequence. No temporal model, no per-frame noise, no training required for the base pipeline. The seed is the authored input.

---

## 2. Mathematical Foundations

### 2.1 Mobius/Lorentz Conjugacy Classification

Every one-parameter flow on PSL(2,C) falls into one of three conjugacy classes:

| Class | Behavior | Closes? |
|---|---|---|
| **Elliptic** | Pure rotation (great circle) | Yes (exactly) |
| **Hyperbolic** | Re-slices along a boost axis | No (orbit is a hyperbola) |
| **Loxodromic** | Rotation + boost simultaneously | Depends on commensurability |

A PSL(2,C) flow closes *if and only if* its generator is elliptic. A monotone hyperbolic boost *cannot* close -- this is a theorem, not a limitation. The system exposes this honestly.

### 2.2 The Four Kinds of Time

The system implements four operator modes, corresponding to physically distinct time behaviors:

| Mode | Class | Formula | Closes? | Character |
|---|---|---|---|---|
| **Camera** | Elliptic | Great circle orbit around e0 | Yes | Viewpoint rotation of a frozen form. No scene time. |
| **Boost (monotone)** | Hyperbolic | Rapidity ramps 0 to A | No | Genuinely re-slices the 4D block, but the orbit is a hyperbola. Seam != 0 (shown, not hidden). |
| **Boost (oscillatory)** | Hyperbolic | phi = A * sin(theta) | Yes | Re-slices AND closes. Palindromic (breathes out and back). Price: no time-arrow. |
| **Screw** | Loxodromic | Oscillatory boost + commensurate spin | Yes (integer spin) | Both re-slice and rotate. Torus knot through the block. |

### 2.3 Scale-Dependent Smoothness

The authored angle theta is a continuous coordinate whose smoothness is scale-dependent:
- **Composition (layout)** moves smoothly: power-law exponent alpha ~= 0.92
- **Detail** evolves roughly: alpha ~= 0.62

This means authored *composition motion* is smooth (coherent video), while detail is the natural carrier of variation. The signal lives at exactly the scale where authored video wants it.

---

## 3. The Moment Operators (Latent Space)

### 3.1 Basis Construction

Four seeds (seed-a, seed-b, seed-c, seed-d) generate four latent vectors, which are Gram-Schmidt orthonormalized into four directions (e0, e1, e2, e3). The orbit operates in the planes spanned by these directions.

```python
raw = [mk(s).flatten().float() for s in (seed_a, seed_b, seed_c, seed_d)]
r = raw[0].norm()
e0, e1, e2, e3 = gram_schmidt(raw)
```

When a discovered axis is provided (`--axis-file`), e0 and the motion plane come from the axis-finding probe instead of seed-derived directions.

### 3.2 The `latent_at(theta)` Function

This is the heart of the kinematic pipeline:

**Elliptic** -- a small circle of angular radius `arc` around e0:
```python
f = (cos(arc) * e0 + sin(arc) * (cos(theta) * e1 + sin(theta) * e2)) * r
```
- `arc = pi/2` gives a great circle (maximum identity change)
- Small `arc` gives a tight, identity-preserving orbit

**Oscillatory** -- arc position s(theta) = A * sin(theta), palindromic:
```python
s = amp * sin(theta)
f = (cos(s) * e0 + sin(s) * e1) * r
```
- Closes exactly: sin(0) = sin(2*pi) = 0
- No time-arrow: the motion breathes out and back

**Screw** -- torus knot coupling two commensurate rotations:
```python
f = (r / sqrt(2)) * (cos(spin*theta)*e0 + sin(spin*theta)*e1
                    + cos(theta)*e2 + sin(theta)*e3)
```
- Closes when `spin` is an integer (commensurability)
- Norm held constant at r

### 3.3 Arc-Length Pacing (`--pace`)

Uniform theta in latent space produces *non-uniform image change* (FLUX's map stretches unevenly), so the motion jumps. The `--pace` flag fixes this:

1. **Measure pass**: render the full loop cheaply (fewer denoising steps, e.g. 6), at the latent's native resolution (the latent shape is fixed by the operator/axis -- speed comes from fewer steps, not a smaller canvas). Downsample only the *output* to 64x64 for the measure.
2. **Compute per-step image change** around the closed loop (wrapping: frame N connects to frame 0):
   ```python
   diffs = [|meas[(i+1) % N] - meas[i]|.mean() for each i]
   ```
3. **Cumulative arc-length** and reparameterization:
   ```python
   cum = cumsum(diffs)
   targets = linspace(0, total_arc, N)
   thetas = interp(targets, cum, theta_extended)
   ```
4. Result: every final frame is an equal *visual* step. No jumps.

### 3.4 Organic Rhythm (`--rhythm organic`)

Phase-warps the loop so the motion is not clockwork. Pure sine at constant d(theta)/dt is simple harmonic motion -- a pendulum, a clock. Organic rhythm breaks this:

```python
k = min(max(skew, 0.0), 0.6)  # monotonicity guard
u = thetas / (2 * pi)
u = u - k*sin(2*pi*u)/(2*pi) - 0.5*k*sin(4*pi*u)/(4*pi)
thetas = 2 * pi * u
```

- The `-sin(2*pi*u)` term eases velocity (lingers at extremes, fast through middle)
- The `-sin(4*pi*u)` term breaks contraction/recovery symmetry
- `skew <= 0.6` keeps du'/du > 0 (monotonic), so the loop still closes
- The result: fast power-stroke, slow recovery drift -- an asymmetry that reads as alive

---

## 4. FLUX.1 vs FLUX.2 -- Architecture Differences

> **Deep reference**: see [`FLUX_ARCHITECTURE.md`](docs/research/FLUX_ARCHITECTURE.md) for the
> full internal pipeline — text encoding mechanisms (CLIP+T5 vs Mistral 3.2),
> VAE internals (ResBlocks, patchify, latent normalization), DiT block structure
> (double/single-stream, AdaLN modulation), flow matching math, and noise
> initialization modes. This section covers only the project-relevant deltas.

| Aspect | FLUX.1-dev | FLUX.2-dev |
|---|---|---|
| **Used by** | `moments_flux.py`, `find_motion_axis.py` | `flux_server.py`, experiment loop |
| **Pipeline class** | `FluxPipeline` (diffusers) | `Flux2Pipeline` (diffusers) |
| **Text encoder** | CLIP-L + T5-XXL (fp16) | Mistral-3 (~45 GB) |
| **Transformer** | ~12 GB | ~61 GB (full bf16, NEVER quantized) |
| **Transformer in_channels** | 64 (16 * 4 via 2x2 pack) | 128+ |
| **VAE latent channels** | 16 | 128 (unpacked) |
| **VAE scale factor** | 8 | Larger (vsf * 2 for spatial) |
| **Latent spatial (unpacked, @512px)** | (16, 64, 64) | (128, 32, 32) |
| **Seed-to-layout coupling** | 2.82% | 3.72% |
| **Loading** | From ComfyUI single-file safetensors | From HF pretrained directory |
| **Memory strategy** | `enable_model_cpu_offload()` | Text encoder quantized (4bit NF4 default); transformer full bf16 on CUDA |
| **Flow-matching** | Rectified-flow ODE | Rectified-flow ODE |

### 4.1 FLUX.1 Loader (`flux1_loader.py`)

FLUX.1-dev exists only as ComfyUI single-file safetensors, not a diffusers directory. `flux1_loader.py` assembles a full `FluxPipeline` from four local components:

| Component | File | Class |
|---|---|---|
| Transformer | `~/ComfyUI/models/unet/flux1-dev.safetensors` | `FluxTransformer2DModel` |
| VAE | `~/ComfyUI/models/vae/ae.safetensors` | `AutoencoderKL` |
| T5 | `~/ComfyUI/models/clip/t5xxl_fp16.safetensors` | `T5EncoderModel` |
| CLIP | `~/ComfyUI/models/clip/clip_l.safetensors` | `CLIPTextModel` |

Uses non-gated config/tokenizer sources (`openai/clip-vit-large-patch14`, `google/t5-v1_1-xxl`) since the FLUX.1 repo is gated.

### 4.2 FLUX.2 Server (`flux_server.py`)

A FastAPI server replacing ComfyUI for headless FLUX.2-dev inference. Key details:

- **Memory**: ~106 GB total (45 GB Mistral-3 + 61 GB transformer) > 96 GB VRAM. Text encoder is quantized to 4bit NF4 (~12 GB); transformer stays full bf16 on CUDA. GH200 unified memory makes CPU offload cheap.
- **Quantization rule**: only the text encoder is quantized. The FLUX.2 transformer is *never* quantized -- full bf16 for image quality and clean temporal-adapter training.
- **Two render paths**:
  - `POST /api/prompt` -- async, queued, WebSocket progress + preview (drives comfort-ui)
  - `POST /api/flux` -- synchronous, writes frames to caller's output_dir (drives experiment harness)
- **Latent injection**: when `latents_b64` is provided (base64 safetensors), it's decoded, validated against the resident grid shape, and passed as `latents=` to each frame. The injected field IS the noise; no fresh generator is created (the old bug).
- **`/api/latent_grid`**: returns the pipe's unpacked latent grid shape (C, Hl, Wl) for a given image size. Clients query this to size their blobs correctly.

### 4.3 Temporal Structure Sources (FLUX.2)

Temporal structure in the FLUX.2 server has two orthogonal sources that compose independently:

1. **Structural-mode noise schedule** injected as `latents=` (M4 FFT phase ramp, validated DPS 0.658). Client-side computation; the server does no noise math.
2. **Trained temporal adapter** (LoRA). Applied via `load_lora_weights()` at configurable strength (default 0.02).

Look (LoRA/adapter) and movement (injected latents field) are independent. Absent both, frames are independent FLUX.2 text-to-image generations.

---

## 5. The Find-to-Drive Pipeline

The central pipeline has two stages: FIND (discover a scene's natural motion axis) and DRIVE (render a full loop along that axis).

### 5.1 FIND: `find_motion_axis.py`

**Thesis**: a scene's natural motion is NOT something the operator imposes; it is a *direction already present* in FLUX's latent around that scene. This script finds the axis instead of guessing it.

**Procedure**:
1. Load FLUX.1-dev, generate a base latent from the scene seed
2. Create N candidate latent directions (orthonormalized against e0 and each other via Gram-Schmidt, offset seeds)
3. For each direction, render a short probe arc (few frames) along that direction
4. Decompose each frame into three bands:
   - **full**: full resolution
   - **mid**: 48x48 (structured motion scale)
   - **lay**: 8x8 (layout/composition scale)
5. Score per direction:
   - **coherence** = mid_move / full_move -- how much motion is structured vs noise
   - **identity** = lay_disp -- how far the scene drifts from itself
   - **structure** = mid_move -- something real moves (not frozen, not shimmer)
   - **sieve score** = (mid_move * coherence) / (lay_disp + epsilon)
6. Save axis basis to `.npz` (e0 + discovered directions + radius + H/W)
7. Save probe frames to `/tmp/find_look/` for human review

**Critical design point**: the sieve is a *starting point for the eye*, NOT a winner. A scalar cannot tell a living motion from a high-scoring dead one. The numbers only surface candidates; they never decide. "The find is made by looking, then by the owner."

### 5.2 DRIVE: `moments_flux.py`

The main render engine. Takes either seed-derived or discovered directions and traces a closed kinematic loop through FLUX's latent space.

**Full pipeline**:
```
[axis file or seeds] -> orthonormal basis (e0..e3)
                     -> latent_at(theta) for theta in [0, 2*pi)
                     -> [optional] --pace: measure pass + arc-length reparameterization
                     -> [optional] --rhythm organic: phase-warp for asymmetric stroke
                     -> render each frame via FluxPipeline
                     -> encode to VP9 webm via ffmpeg
                     -> [optional] write job state for Motion tab
```

**CLI arguments**:
```
--prompt        Text prompt
--mode          elliptic | oscillatory | screw
--amp           Oscillatory arc amplitude (radians)
--arc           Elliptic angular radius (pi/2 = great circle)
--spin          Screw integer turns (commensurate => closes)
--rhythm        linear | organic
--skew          Organic rhythm strength (0-0.6)
--axis-file     Discovered axis basis (.npz from find_motion_axis.py)
--axis-plane    Which two discovered dirs form the motion plane
--pace          Enable arc-length pacing (measure pass + reparameterize)
--pace-steps    Denoise steps for cheap measure pass (default 6)
--frames        Frame count (default 24)
--steps         Denoise steps per frame (default 8)
--size          Render size in px (default 512)
--fps           Output framerate (default 12)
--seed-a/b/c/d  Four basis seeds
--model         Model path (default FLUX.1-dev)
--out           Output .webm path
--job-id        If set, write state so Motion tab tracks this render
```

### 5.3 Pipeline Flow Diagram

```
find_motion_axis.py              moments_flux.py                 [FLUX model]
     (FIND)                          (DRIVE)                      (RENDER)
        |                               |                            |
 Probe latent                   Load discovered axis           FluxPipeline
 directions around              OR use seed-derived                  |
 a fixed scene                  basis (e0..e3)              Per-frame inference
        |                               |                  with latents= kwarg
 Score by:                       Apply kinematic                     |
 structure * coherence /         operator: elliptic /          Images -> PNG
 displacement                    oscillatory / screw                 |
        |                               |                    ffmpeg VP9 encode
 Save .npz axis basis            For each theta:                     |
 (e0 + dirs + r + H/W)          - latent_at(theta)              Output .webm
        |                        - Optionally PACE                   |
 Save probe frames               - Optionally ORGANIC          Ingest into
 for human review                  rhythm                    Renders gallery
```

---

## 6. FLUX.2 Experiment Infrastructure

### 6.1 Continuous Experiment Loop (`flux_experiment_loop.py`)

A systemd service (`fluxloop.service`) that keeps the GPU busy by cycling a sweep. Each pass:

1. **Train** a temporal adapter via `train_lite` (LoRA, cycling through ranks [4, 8, 16, 32, 64], 300 steps)
2. **Render** 24 frames at 512x512 with 8 denoise steps via `POST /api/flux`
3. **Motion gate** -- the VJ-grade gate computes:
   - `adjacent_mean_delta` -- per-frame pixel change
   - `first_last_delta` -- loop closure metric
   - `DPS` (Directional Persistence Score) -- the morph-vs-motion discriminator
   - `MMD`, `SSEM`, `dominant_direction_fraction`
4. **Write** `result.json` + rebuild `experiments/index.json` (consumed live by the UI)

Sweep configuration:
```python
RANKS = [4, 8, 16, 32, 64]
STEPS = 300
FRAMES, W, H, RSTEPS = 24, 512, 512, 8
SLEEP_BETWEEN = 0.0  # no idle gap -- keep the GPU saturated
```

### 6.2 Single-Run Experiment (`run_flux_experiment.py`)

Same four-step pipeline (train -> render -> gate -> result.json) for one-off experiments. Fixed config: rank 16, 300 steps, 12 frames, 512x512.

### 6.3 Measurement Language (The Ladder)

The experiment dashboard uses a six-rung measurement ladder, each rung a macro question:

| Rung | Question | Metric | Good |
|---|---|---|---|
| **Hold** | Do you want to keep looking? | Overall loop grade (smoothness, penalized by seam) | High |
| **Aliveness** | Does it resemble life? | Divergence-free share of optical flow (fluid/wind/breath vs tear/explode) | High (toward 1) |
| **Pace** | Is it too fast? | Average pixel change between frames | Calm band above 0.037 |
| **Smoothness** | Is it smooth? | Fraction of moving area that glides vs jitters | High |
| **Seam** | Does it jar where it loops? | First-to-last change / normal frame step (~1 = seamless) | About 1 |
| **Form-hold** | Does the shape hold? | Distance from last frame to first | Low |

---

## 7. Frontend Surfaces

### 7.1 Moments View (`moments/index.tsx`)

The main UI. Two halves:

**A. The SDF Instrument** (WebGL2 live shader):
- A genuine 4D gyroid block on R^3 x S^1, sliced live by the moment operator
- Four time-mode buttons (Camera, Boost up, Boost oscillatory, Screw)
- Drag-to-orbit camera with scroll zoom
- Controls: Rapidity A slider, Spin turns (screw only), Loop period, play/pause
- Measurements: seam (frame@0 vs frame@period, in /255), image-change rate (mid-loop delta)
- Capture loop button: records one full period via MediaRecorder -> WebM -> Renders gallery

**B. The "Author via FLUX" Panel**:
- Maps UI moment modes to FLUX latent-space operators:
  - Camera -> elliptic
  - Screw -> screw
  - Both boosts -> oscillatory
- Parameters: prompt textarea, scene seed + shuffle, quality presets:
  - Draft: 8 steps, 512px
  - Standard: 16 steps, 576px
  - High: 24 steps, 640px
- POSTs to `/api/render/moments-flux`, polls for progress
- Finished renders land in the Renders gallery with origin `moments-flux`

### 7.2 The 4D Gyroid Shader (`moments/shader.ts`)

GLSL fragment shader implementing the SDF instrument:

**4D Gyroid field**:
```glsl
float a = 2.6;
float G = sin(a*P.x)*cos(a*P.y) + sin(a*P.y)*cos(a*P.z)
        + sin(a*P.z)*cos(a*P.w) + sin(a*P.w)*cos(a*P.x);
```
- `w` enters with the SAME frequency as the space axes. This was a deliberate correction: when `w` was "slow" (frequency 1.0), the boost re-sliced ~4x less visibly than a camera rotation. Matching frequencies makes a boost re-slice as visually significant as a rotation displacement.
- The gyroid is genuinely 4D (`dB/dw != 0`). The motion of its 3D slices is intrinsic to the field, not hand-animated as `cos(w)*position`.

**Slice embedding** (`sliceEmbed`):
```glsl
// boost (z,w=0) -> (z cosh(phi), z sinh(phi))
float z = p.z * cosh(phi);
float w = p.z * sinh(phi);
// screw adds (x,y) rotation
vec2 xy = mat2(cr,-sr,sr,cr) * p.xy;
```
Uses actual `cosh`/`sinh`, not approximations. The gyroid's w-periodicity via cos/sin means no modular arithmetic is needed.

**Lipschitz correction**: a boost stretches space by e^|phi|, so the pulled-back SDF's gradient blows up and a naive ray march tunnels. Each step is divided by the Lipschitz bound:
```glsl
float lip = exp(abs(phiG));
t += max(d/lip, 0.0008) * 0.9;
```

**Color per mode**: Camera=#4570FF, Monotone=#FF9E66, Oscillatory=#7045FF, Screw=#FF55C6

### 7.3 Motion View (`motion/index.tsx`)

The "Moments-to-FLUX spiral" dashboard:

- **Live Experiments**: polls `GET /api/render/moments-flux -> { jobs }`. Shows running jobs with kind-of-time, parameters, live-ticking duration. Recent completed jobs below.
- **The Spiral**: research documents (Ledger, Formula, The Math, Measured) served from `/api/render/spiral`. The ledger is live-polled while renders run (it grows each cycle).
- **Gallery**: finished renders filtered from the renders store by `origin === 'moments-flux'`, newest first. Each card shows the mode (elliptic/oscillatory/screw) with its color tint.

### 7.4 Flux Experiments Tab (`flux_experiments.tsx`)

Live view of the continuous FLUX.2 experiment loop:

- Polls `/experiments/index.json` and `/experiments/analysis.json` every 5 seconds
- Groups experiments by family, capped at 12 videos per group
- Ordered by best Hold (descending)
- Each card: looping video, PASS/fail gate badge, Hold headline metric, chip strip (Aliveness, Smoothness, Seam)
- Click-to-open detail modal walks through all six rungs of the measurement ladder with values, explanations, and "good" targets

---

## 8. Server Middleware (`vite.config.ts`)

### 8.1 `momentsFluxPlugin`

Vite dev server middleware wiring the moments-flux API:

- **`POST /api/render/moments-flux`**: validates parameters, spawns `python3 server/moments_flux.py` as a detached child process. One render at a time (FLUX needs the whole GPU). Seeds are derived deterministically from the scene seed with fixed offsets (+16, +44, +82). Environment: `HF_HUB_OFFLINE=1`, `TRANSFORMERS_OFFLINE=1`.
- **`GET /api/render/moments-flux?job=<id>`**: polls the log file for progress (`frame N/M`), detects `RESULT {...}` line for completion, ingests the finished webm into the universe-renders gallery.
- **`GET /api/render/moments-flux`**: lists all recent jobs sorted by start time (capped at 12).
- State stored in `.moments-flux/` directory (gitignored).

### 8.2 `spiralDocsPlugin`

Serves research documents (Ledger, Formula, The Math, Measured) from `~/MotionBridge/docs/` into the Motion view, so the research is visible where the renders are.

---

## 9. Measured Properties

These measurements ground the system's claims in empirical data:

| Property | FLUX.1 | FLUX.2 | Method |
|---|---|---|---|
| **Bit-determinism** | `|delta| = 0.00000` | `|delta| = 0.00000` | Same seed, two runs, pixel diff |
| **Seed-to-layout coupling** | 2.82% | 3.72% | Layout-band displacement under seed perturbation |
| **Elliptic closure** | 0.003 | -- | latent(0) vs latent(2*pi), L2 norm |
| **Composition smoothness** | alpha ~= 0.92 | -- | Power-law exponent of layout-band change vs theta |
| **Detail roughness** | alpha ~= 0.62 | -- | Power-law exponent of full-resolution change vs theta |
| **DPS (validated render)** | -- | 0.658 | Directional Persistence Score on M4 FFT phase ramp |

**Bit-determinism** means the seed is a genuine coordinate, not randomness. **Low coupling** means small latent moves change detail, not layout. Together they make authored latent-space trajectories viable as a video pipeline.

---

## 10. Offline / Tokenless Operation

Both the moments-flux engine and the FLUX.2 server run with `HF_HUB_OFFLINE=1` and `TRANSFORMERS_OFFLINE=1`. Weights are cached locally:

- FLUX.1: ComfyUI single-file safetensors in `~/ComfyUI/models/`
- FLUX.2: HuggingFace pretrained directory at `/home/ubuntu/models/flux2-dev`

No HuggingFace token is read or written at render time. This is a hard constraint.

---

## 11. CLI Entry

The Go CLI (`cli/render/registry.go`) has a separate `flux` render entry that runs `overtone/motion_flow.py`. This is *not* the FLUX diffusion model -- it renders optical-flow color-coded velocity fields (pan, zoom, swirl, focus) as single stills representing continuous motions. The naming collision is coincidental.

---

## 12. Key Files Index

| File | Role |
|---|---|
| `ui/comfort-ui/server/moments_flux.py` | Core render engine: authored latent loop -> FLUX frames -> webm |
| `ui/comfort-ui/server/find_motion_axis.py` | Axis discovery: probe latent directions, score, save .npz |
| `ui/inference/flux_server.py` | FLUX.2 FastAPI inference server (replaces ComfyUI) |
| `ui/inference/flux1_loader.py` | FLUX.1 pipeline assembly from ComfyUI single-files |
| `ui/inference/experiment_loop/flux_experiment_loop.py` | Continuous FLUX.2 sweep: train -> render -> gate -> result |
| `ui/inference/experiment_loop/run_flux_experiment.py` | Single-run experiment (same pipeline, one shot) |
| `ui/comfort-ui/src/designs/render/views/moments/index.tsx` | Moments view: SDF instrument + "author via FLUX" panel |
| `ui/comfort-ui/src/designs/render/views/moments/shader.ts` | 4D gyroid GLSL shader + mode definitions |
| `ui/comfort-ui/src/designs/render/views/moments/README.md` | Documentation: thesis, measurements, constraints |
| `ui/comfort-ui/src/designs/render/views/motion/index.tsx` | Motion view: live experiments + render gallery |
| `ui/comfort-ui/src/designs/flux_experiments.tsx` | Flux experiments tab: dashboard + measurement ladder |
| `ui/comfort-ui/vite.config.ts` (lines 1219-1311) | `momentsFluxPlugin`: POST/GET endpoints for moments-flux jobs |
