diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5dec4588..393ebdd3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -95,6 +95,14 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Under the hood
+- **The builder's CONDITIONS page.** A new page between NPCS and TRY IT lists the ten standard
+ conditions with on/off switches and the system's own below them. Each can be renamed (the
+ standard name shows until you type one), given a short label for its chip, an icon (24 drawn for
+ CITY_NET that follow the theme, or a picture you upload), a description, an end (when removed, or
+ after a number of rounds) and modifiers on all rolls, a stat or a formula. + CONDITION adds one of
+ your own, up to 60 in all, and deleting one asks first. Putting a standard condition back as it
+ was saves nothing, as with WORDS. Conditions on tokens come next.
+
- **Conditions as part of a game system.** A custom system now carries its conditions: ten
standard ones every system starts with (Blinded, Bleeding, Poisoned, Prone, Stunned, Grappled,
Frightened, Unconscious, Restrained, Exhausted), each of which can be renamed, described, given
diff --git a/README.md b/README.md
index cb3267e3..e52a3af3 100644
--- a/README.md
+++ b/README.md
@@ -709,6 +709,8 @@ CITY_NET/
│ │ │ ├── SheetPage.tsx # CHARACTER SHEET: automatic until CUSTOMIZE copies the starter; a tree of tabs, sections and fields to move and edit, who sees and who changes each field, a tray of stats not placed yet, and a live preview. Also edits NPCS' stat block of its own (GM-only preview)
│ │ │ ├── SheetPreview.tsx # The sheet designer's live preview, drawn by the real SheetRenderer, as its owner, the GM and everyone else see it
│ │ │ ├── NpcsPage.tsx # NPCS: the stat block (the character sheet, or one of its own) and TIERS, each box a number, a formula with @level or dice, typed text kept as typed, a TRY IT roll at a level
+│ │ │ ├── ConditionsPage.tsx # CONDITIONS: the standard set with switches and a system's own (+ CONDITION, DELETE after asking), up to 60; each one's name (the standard name as placeholder), chip label, icon (24 drawn, or UPLOAD), description, how it ends (when removed or after rounds) and modifiers on all rolls, a stat or a formula, typed numbers kept as typed
+│ │ │ ├── ConditionIcon.tsx # A condition's icon: one drawn for CITY_NET, stroked in the theme's color, or an uploaded picture through only; anything unknown draws the target
│ │ │ ├── TryItPage.tsx # TRY IT: the draft as a player would meet it, unsaved changes included. A made-up character on its own sheet with formulas worked out as you type, its health on a pretend token in the HEALTH folder's own panels with what others see, and an NPC rolled from a tier. Saves nothing but SAVE AS THE SAMPLE CHARACTER
│ │ │ ├── ControlledNpcSheetWindow.tsx # A friendly NPC's sheet for the player the GM gave it to: read-only, with rolls made for the NPC through the server
│ │ │ ├── UpdateModal.tsx # Draggable update notification modal (shown on admin login when update available; Update Now / Remind Me Later / Skip Version; docker-aware)
@@ -780,6 +782,7 @@ CITY_NET/
│ │ │ ├── StatsRulesPage.test.tsx # Stats in groups with their SAMPLE column, formulas with live values and mistakes, and tables with TRY IT
│ │ │ ├── SheetPage.test.tsx # Automatic or customized, the tree of tabs, sections and fields, moving and removing them (sections going to a tab you choose, the last tab making one page), and each field's settings
│ │ │ ├── NpcsPage.test.tsx # The stat block shared or its own, tiers whose boxes keep what was typed, and TRY IT rolling one, all through the server's own code
+│ │ │ ├── ConditionsPage.test.tsx # Switches storing only "off", renaming and putting back, icons drawn or uploaded (a refusal said), ends and rounds, modifiers typed negative, a system's own added, named, deleted after asking, and + CONDITION stopping at 60
│ │ │ ├── TryItPage.test.tsx # The made-up character and its formulas, SAVE AS THE SAMPLE (absent with nowhere to save), RESET, health on a pretend token under each model, both maximums, and an NPC rolled from a tier
│ │ │ ├── ControlledNpcSheetWindow.test.tsx # A friendly NPC's sheet read-only for the player it was given to, fetched with their own login, rolling as the NPC
│ │ │ ├── SheetAttackPanel.attackAs.test.tsx # ATTACK AS: a player with a friendly NPC given to them, the GM with an NPC near the target, its weapons and LUCK, firing as it
@@ -923,6 +926,8 @@ CITY_NET/
│ │ │ ├── sheetDesigner.ts # CHARACTER SHEET as logic: a system's own sheet read from its definition and written back - tabs, sections, fields, placement, the tray, and the problems that stop it saving
│ │ │ ├── publicLines.ts # What everyone else sees of a custom system's character: the sheet's public lines, for ID.EXE's INFO and the sheet preview's EVERYONE ELSE
│ │ │ ├── npcs.ts # NPCS as logic: the stat block (shared or its own), tiers and their boxes (number, formula with @level, or dice), the server's limits, and a rolled box as text
+│ │ │ ├── conditions.ts # CONDITIONS as logic: the standard set and a system's own read from a definition and written back storing only what differs (a standard one put back leaves nothing), the count against 60, new and deleted ones, and what a modifier may name; mirrors the server's conditions.js
+│ │ │ ├── conditionIcons.ts # The 24 condition icons drawn for CITY_NET as path data on a 24-pixel grid, ours to use (the four first sketched too close to Feather's redrawn), and telling a drawn icon from an uploaded one
│ │ │ ├── tryIt.ts # TRY IT as logic: the made-up character from the sample, typed values, SAVE AS THE SAMPLE, the pretend token and its maximum
│ │ │ ├── examples.ts # The examples' and starters' cards (health, advancement in the system's own XP word, stats, formulas, distance), the copy's suggested name, and locking a page while one is looked at (every control but tabs, things opening out and marked views)
│ │ │ ├── templates/
@@ -942,6 +947,8 @@ CITY_NET/
│ │ │ ├── statsRules.test.ts # Stats, samples, formulas and tables written back, ids from first names and kept after, held to the server's check and engine
│ │ │ ├── sheetDesigner.test.ts # The sheet designer's every step (customize, the tray, moving and removing tabs, sections and fields, attack numbers never EVERYONE), held to the server's check
│ │ │ ├── npcs.test.ts # The stat block and tiers written back, held to the server's check and rolled by its tierRolls
+│ │ │ ├── conditions.test.ts # The standard set and what the game offers held to the server's own conditionsOf, each change storing only what differs, a system's own added, renamed and deleted, the limit of 60, and what a modifier may name
+│ │ │ ├── conditionIcons.test.tsx # The 24 icons are the server's, path data alone, none of Feather's paths, stroked in currentColor and never filled; an upload drawn through ; anything unknown drawn as the target
│ │ │ ├── tryIt.test.ts # The made-up character, typed values, SAVE AS THE SAMPLE and RESET, the pretend token and its maximum
│ │ │ ├── examples.test.ts # The examples' and starters' card facts read off the server's own definitions, XP in the system's word, and locking a page but for browsing
│ │ │ ├── customTemplates.test.tsx # A published custom system's sheet drawn by the ordinary renderer from the server's render copy, the generic template standing in until it loads
diff --git a/frontend/src/components/BuilderScreen.tsx b/frontend/src/components/BuilderScreen.tsx
index 678aee9e..21ae4c2c 100644
--- a/frontend/src/components/BuilderScreen.tsx
+++ b/frontend/src/components/BuilderScreen.tsx
@@ -13,6 +13,8 @@ import { FeaturesPage } from './FeaturesPage';
import { StatsRulesPage } from './StatsRulesPage';
import { SheetPage } from './SheetPage';
import { NpcsPage } from './NpcsPage';
+import { ConditionsPage } from './ConditionsPage';
+import { CONDITION_ICONS } from '../sheets/conditionIcons';
import { TryItPage } from './TryItPage';
// The system builder (4a2b): it takes over the whole window, with no map. A sidebar down the left
@@ -115,6 +117,7 @@ const Icons: Record}
{ready && page === 'sheet' && }
{ready && page === 'npcs' && }
+ {ready && page === 'conditions' && }
{ready && page === 'try' && }
diff --git a/frontend/src/components/ConditionIcon.tsx b/frontend/src/components/ConditionIcon.tsx
new file mode 100644
index 00000000..06c7d239
--- /dev/null
+++ b/frontend/src/components/ConditionIcon.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { CONDITION_ICONS, isDrawnIcon, isUploadedIcon } from '../sheets/conditionIcons';
+
+// A condition's icon (4e1b): one drawn for CITY_NET, stroked in the theme's color, or a picture the
+// GM uploaded, drawn through only, which is what makes an uploaded SVG safe (as currency
+// icons and battle maps). Anything else draws the target, so a broken reference still shows a mark.
+
+export function ConditionIcon({ icon, size = 18, title }: { icon: string; size?: number; title?: string }) {
+ if (isUploadedIcon(icon)) {
+ return ;
+ }
+ const paths = CONDITION_ICONS[isDrawnIcon(icon) ? icon : 'target'];
+ return (
+
+ );
+}
diff --git a/frontend/src/components/ConditionsPage.tsx b/frontend/src/components/ConditionsPage.tsx
new file mode 100644
index 00000000..3550c36e
--- /dev/null
+++ b/frontend/src/components/ConditionsPage.tsx
@@ -0,0 +1,214 @@
+import React, { useEffect, useRef, useState } from 'react';
+import type { Definition, systemsApi } from '../sheets/systemsApi';
+import {
+ conditionList, conditionCount, withCondition, withNewCondition, withoutCondition, modifierTargets,
+ LIMITS, ALL_ROLLS, STANDARD, type Listed,
+} from '../sheets/conditions';
+import { CONDITION_ICON_IDS, isUploadedIcon } from '../sheets/conditionIcons';
+import { ConditionIcon } from './ConditionIcon';
+
+// The builder's CONDITIONS page (4e1b; approved mockup docs/mockups/builder-conditions.html,
+// 2026-10-09): what can happen to a character besides losing health. The standard set, each with
+// a switch, edited or turned off; the system's own, added and deleted; each one's name, chip label,
+// icon (drawn, or uploaded), description, how it ends, and its modifiers. What it reads and writes
+// is sheets/conditions.ts; every change goes through the builder's `edit`.
+
+interface Props {
+ definition: Definition;
+ edit: (next: Definition) => void;
+ /** For uploading an icon; without it only the drawn icons are offered. */
+ api?: ReturnType;
+}
+
+const small: React.CSSProperties = { fontSize: 10, letterSpacing: 2, opacity: 0.75 };
+const why: React.CSSProperties = { fontSize: 12, lineHeight: 1.45, opacity: 0.85, margin: 0 };
+const btn: React.CSSProperties = { fontFamily: 'monospace', fontSize: 11, letterSpacing: 1, padding: '4px 9px' };
+const input: React.CSSProperties = {
+ background: 'var(--black)', color: 'var(--green)', border: '1px solid var(--green)', fontFamily: 'monospace', fontSize: 12, padding: '4px 6px', minWidth: 0, boxSizing: 'border-box',
+};
+const row: React.CSSProperties = { display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' };
+
+/**
+ * A whole number box that keeps what is typed: a lone minus sign, or a field cleared to type
+ * again, stays as typed rather than becoming 0 under the cursor.
+ */
+function WholeBox({ aria, value, min, max, width = 64, onChange }: { aria: string; value: number; min: number; max: number; width?: number; onChange: (n: number) => void }) {
+ const [text, setText] = useState(String(value));
+ useEffect(() => { setText((t) => (Number(t) === value ? t : String(value))); }, [value]);
+ return (
+ {
+ setText(e.target.value);
+ if (/^-?\d+$/.test(e.target.value.trim())) onChange(Math.max(min, Math.min(max, parseInt(e.target.value, 10))));
+ }} />
+ );
+}
+
+function Switch({ on, label, onChange }: { on: boolean; label: string; onChange: (on: boolean) => void }) {
+ return (
+
+ );
+}
+
+function Detail({ c, definition, edit, api }: Props & { c: Listed }) {
+ const [confirm, setConfirm] = useState(false);
+ const [uploading, setUploading] = useState(false);
+ const [uploadError, setUploadError] = useState(null);
+ const fileRef = useRef(null);
+ const base = STANDARD.find((s) => s.id === c.id);
+ const targets = modifierTargets(definition);
+ const set = (patch: Parameters[2]) => edit(withCondition(definition, c.id, patch));
+ const stored = (definition.conditions as Record> | undefined)?.[c.id] ?? {};
+ const typed = (key: 'name' | 'short') => (typeof stored[key] === 'string' ? stored[key] as string : (base ? '' : c[key]));
+
+ const upload = async (file: File | undefined) => {
+ if (!file || !api) return;
+ setUploading(true);
+ const r = await api.uploadIcon(file, 'condition');
+ setUploading(false);
+ if (!r.ok) { setUploadError(r.error); return; }
+ setUploadError(null);
+ set({ icon: r.value.icon });
+ };
+
+ return (
+
+
+ {base ? 'STANDARD CONDITION' : 'ONE OF THIS SYSTEM\'S OWN'}
+ {base
+ ? {c.on ? 'On: offered for tokens.' : 'Off: not offered for tokens.'}
+ : confirm
+ ?
+ Delete {c.name || 'this condition'}?
+
+
+
+ : }
+