# Science designer UX A *science* is a named mix of four tech proportions — `drive`, `weapons`, `shields`, `cargo` — that sum to `1.0`. When a planet's production is set to a science, the planet's industry output for that turn is split between the four tech research tracks in those proportions (`game/internal/controller/planet/production.go.runScienceResearch`). The CRUD list, the designer, and the production-picker integration are provided by the UI; the wire and engine validation are handled by the backend. ## Engine semantics in one paragraph `pkg/schema/fbs/order.fbs.CommandScienceCreate` carries `name + drive + weapons + shields + cargo` as four `float64` proportions. The engine validator (`pkg/calc/validator.go.ValidateScienceValues`) refuses any value outside `[0, 1]` and any sum that drifts further than its float tolerance from `1.0`. Names follow the universal entity-name rules (`pkg/util/string.go.ValidateTypeName`): trimmed, non-empty, ≤ 30 runes, only letters / digits / combining marks / the allowed special set `!@#$%^*-_=+~()[]{}`, no special at start or end, ≤ 2 specials in a row, no whitespace. There is no `CommandScienceUpdate` on the wire — sciences are write-once, and an "edit" is a Remove + Create sequence. ## Percent input model The designer presents the four proportions as percentages (`step="0.1"`, range `[0, 100]`) so the player can type and reason about whole-number splits — closer to how `site/ru/rules.md` describes sciences (`site/ru/rules.md` #sciences: "10 parts Drive, 5 parts Weapons, 30 parts Shields, 0 parts Cargo, …"). The wire shape is still fractions; conversion happens inside `validateScience` only on Save (`value / 100` for each of the four). The four inputs are *not* auto-rebalanced. The validator refuses a draft whose sum drifts further than `SUM_EPSILON_PERCENT` (`1e-3`) from `100`, and the form's Save button stays disabled until the sum matches. A live readout under the inputs displays the running total so the player can chase it down without trial-and-error guessing. The strict-sum gate was chosen over alternatives — auto-rebalance and raw-parts-with-engine-normalisation — because keeping the input model close to "what gets sent on the wire" minimises surprises when the engine returns the science exactly as typed. See `lib/util/science-validation.ts` for the validator and the conversion helper. ## Name validation `validateScience` runs `validateEntityName` first and returns its invalid-reason verbatim, so the designer's `aria-describedby` mapping reuses the existing translation keys for `empty`, `too_long`, `starts_with_special`, `ends_with_special`, `consecutive_specials`, `whitespace`, `disallowed_character`. A new key `duplicate_name` covers the UX-only check against the optimistic-overlay `localScience` projection — the engine would refuse the duplicate at submit time, but catching it locally keeps the Save button disabled with a clear hint instead of letting a red-badge `rejected` row land in the order tab. ## Read-only view mode A `scienceId`-bearing URL renders the designer in view mode: a read-only table of the four percentages plus name, with Back and Delete affordances. Sciences are write-once on the wire, so there is no Save-edits affordance — to change a science, the player deletes it and creates a new one. Delete dispatches a `removeScience` order command; the engine refuses removals when the science is referenced by an active production target on any planet, which surfaces as `rejected` in the order tab. ## Production-picker integration The planet inspector's production row (`lib/inspectors/planet/production.svelte`) is two `