code:
/**
 * UI for the Salon. Refreshes without refreshing the passage.
 * @param {App.Entity.PlayerState} PC
 */
App.UI.playerSalon = function(PC) {
	const container = document.createElement("span");
	container.id = "salon";

	container.append(createPage());
	return container;

	function createPage() {
		const el = new DocumentFragment();

		el.append(intro());
		el.append(eyewear());
		if (getBestVision(PC) > 0) {
			if (
				(["leopard", "tiger", "jaguar"].includes(PC.earT) && PC.earTColor !== "hairless") ||
				["leopard", "tiger", "jaguar", "gazelle", "tanuki", "raccoon"].includes(PC.tailShape) ||
				PC.wingsShape === "moth"
			) {
				el.append(pattern());
			}
			el.append(ears());
			if (PC.horn !== "none") {
				el.append(horns());
			}
			el.append(hair());
			/*
			el.append(makeup());
			el.append(nails());
			*/
			el.append(bodyHair());
			if (PC.tail !== "none") {
				el.append(tail());
			}
			/*
			if (PC.appendages !== "none") {
				el.append(appendages());
			}
			*/
			el.append(skin());
		}
		return el;
	}

	function intro() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h1", el, "Personal Salon");
		App.UI.DOM.appendNewElement("div", el, `You unpack your personal salon kit so that you may adjust your appearance.`, "scene-intro");
		return el;
	}

	function eyewear() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Eyewear");
		const r = [];
		const options = new App.UI.OptionsGroup();

		if (getBestVision(PC) === 0) {
			r.push(`You are blind; you have no idea what any of this stuff is. Oh hey! Glasses!`);
		} else if (anyVisionEquals(PC, 1)) {
			r.push(`You are nearsighted`);
		} else {
			r.push(`Your vision is normal`);
		}

		const option = options.addOption(r.join(" "), "eyewear", PC)
			.addValue("None", "none")
			.addValue("Cosmetic glasses", "glasses");
		if (getBestVision(PC) !== 0 && anyVisionEquals(PC, 1)) {
			option.addValue("Corrective glasses", "corrective glasses");
			if (hasAnyEyes(PC)) {
				option.addValue("Corrective contacts", "corrective contacts");
			}
		}
		el.append(options.render());

		el.append(App.Medicine.Modification.eyeSelector(PC));
		return el;
	}

	function pattern() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Pattern");

		el.append(App.Medicine.Salon.pattern(PC));

		return el;
	}

	function ears() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Ears");
		const r = [];
		const options = new App.UI.OptionsGroup();

		if (PC.earImplant === 1) {
			r.push(`You have artificial inner ear implants`);
		} else if (PC.hears < -1) {
			r.push(`You are deaf`);
		} else if (PC.hears > -1) {
			r.push(`Your hearing is normal`);
		} else {
			r.push(`You are hard of hearing`);
		}
		const option = options.addOption(r.join(" "), "earwear", PC)
			.addValue("None", "none");
		// Hard of hearing
		if (PC.hears === -1 && PC.earImplant !== 1) {
			option.addValue("Hearing aids", "hearing aids");
		}

		// Top ear Color
		if (PC.earT !== "none" && PC.earTColor !== "hairless") {
			let title;
			let option;
			let showChoices = true;
			const filtered = App.Medicine.Modification.Color.Primary.filter(c => ![PC.earTColor].includes(c.value));

			title = `You have ${PC.earTColor} ears${PC.earTEffect === "none" ? `.` : ` with ${PC.earTEffect}.`}`;
			App.UI.DOM.appendNewElement("div", el, title);

			if (showChoices) {
				if (PC.earTEffect !== "none" || PC.earTEffectColor !== "none") {
					options.addCustomOption("")
						.addButton("Remove effect", () => {
							PC.earTEffect = "none";
							PC.earTEffectColor = "none";
							App.UI.reload();
						});
				}

				option = options.addOption("Fur Color", "earTColor", PC);
				for (const color of App.Medicine.Modification.Color.Primary) {
					option.addValue(capFirstChar(color.value), color.value);
				}
				option.pulldown();

				option = options.addOption("Effect color", "earTEffectColor", PC);
				for (const color of filtered) {
					option.addValue(capFirstChar(color.value), color.value);
				}
				option.pulldown();

				option = options.addOption("Effect", "earTEffect", PC);
				for (const color of App.Medicine.Modification.Color.Effect) {
					option.addValue(capFirstChar(color.value), `${PC.earTEffectColor} ${color.value}`);
				}
				option.pulldown();
			}
		}
		el.append(options.render());

		return el;
	}

	function horns() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Horns");
		const options = new App.UI.OptionsGroup();

		const option = options.addOption(`Dye your ${PC.horn}`, "hornColor", PC);
		for (const hornColor of App.Medicine.Modification.hornColor) {
			option.addValue(capFirstChar(hornColor), hornColor);
		}

		el.append(options.render());

		return el;
	}

	function makeup() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Makeup");
		const r = [];
		const options = new App.UI.OptionsGroup();

		// Needs descriptions
		options.addOption(r.join(" "), "makeup", PC)
			.addValue("Makeup free", 0)
			.addValue("Nice", 1)
			.addValue("Gorgeous", 2)
			.addValue("Slutty", 4)
			.addValue("Color-coordinate with hair", 3);

		options.addOption("", "makeup", PC)
			.addValue("Neon", 5)
			.addValue("Neon, color-coordinate with hair", 6);

		options.addOption("", "makeup", PC)
			.addValue("Metallic", 7)
			.addValue("Metallic, color-coordinate with hair", 8);

		el.append(options.render());

		return el;
	}

	function nails() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Nails");
		const r = [];
		const options = new App.UI.OptionsGroup();

		// Needs descriptions
		options.addOption(r.join(" "), "nails", PC)
			.addValue("Neatly clipped", 0)
			.addValue("Long and elegant", 1)
			.addValue("Sharp and claw-like", 3)
			.addValue("Bright and glittery", 4)
			.addValue("Very long and garish", 5)
			.addValue("Color-coordinate with hair", 2);

		options.addOption("", "nails", PC)
			.addValue("Neon", 6)
			.addValue("Neon, color-coordinate with hair", 7);

		options.addOption("", "nails", PC)
			.addValue("Metallic", 8)
			.addValue("Metallic, color-coordinate with hair", 9);

		el.append(options.render());

		return el;
	}

	function hair() {
		const el = new DocumentFragment();
		let option;
		App.UI.DOM.appendNewElement("h3", el, "Hair");
		const options = new App.UI.OptionsGroup();
		let title;
		let showChoices = true;
		const hasWig = (PC.bald === 1 && PC.hStyle !== "bald");
		const filtered = App.Medicine.Modification.Color.Primary.filter(c => ![PC.hColor].includes(c.value));

		if (PC.bald === 1) {
			if (PC.hStyle === "bald") {
				title = `You are completely bald.`;
				showChoices = false;
			} else {
				title = `You're wearing a ${PC.hColor}${PC.hEffect === "none" ? `` : ` with ${PC.hEffect}`} wig.`;
			}
		} else {
			title = `You have ${PC.hColor} hair${PC.hEffect === "none" ? `` : ` with ${PC.hEffect}`}.`;
		}
		App.UI.DOM.appendNewElement("div", el, title);

		if (PC.bald === 1) {
			options.addOption(`Use a wig`, "hStyle", PC)
				.addValue("Enable", "neat").on()
				.addValue("Disable", "bald").off();
		}

		if (showChoices) {
			if (PC.hLength > 1) {
				// Color
				if (PC.hEffectColor !== "none" || PC.hEffect !== "none") {
					options.addCustomOption("")
						.addButton("Remove effects", () => {
							PC.hEffectColor = "none";
							PC.hEffect = "none";
							App.UI.reload();
						});
				}
				option = options.addOption("Primary color", "hColor", PC);
				if (PC.origHColor !== PC.hColor) {
					option.addValue("Restore your natural color", PC.origHColor);
				}
				for (const color of App.Medicine.Modification.Color.Primary) {
					option.addValue(capFirstChar(color.value), color.value);
				}
				option.pulldown();

				option = options.addOption("Effect", "hEffect", PC);
				for (const color of App.Medicine.Modification.Color.Effect) {
					option.addValue(capFirstChar(color.value), `${PC.hEffectColor} ${color.value}`);
				}
				option.pulldown();

				option = options.addOption("Effect color", "hEffectColor", PC);
				for (const color of filtered) {
					option.addValue(capFirstChar(color.value), color.value);
				}
				option.pulldown();
			}
			/*
			// Style
			if (PC.hLength > 1) {
				title = `Go get your ${hasWig ? "wig" : "hair"} styled `;
			} else {
				title = `Your ${hasWig ? "wig" : "hair"} is too short to style meaningfully`;
			}
			option = options.addOption(title, "hStyle", PC);
			if (PC.hLength > 1) {
				for (const style of App.Medicine.Modification.hairStyles.Normal) {
					option.addValue(style.title, style.value, billMod);
				}
				option.pulldown();
			}

			// Style + Cut
			if (PC.hLength > 1) {
				option = options.addOption(`${hasWig ? "Change wig style and length" : "Cut and style hair"}`, "hStyle", PC);
				if (PC.hLength > 1) {
					for (const style of App.Medicine.Modification.hairStyles.Cut) {
						option.addValue(
							style.title,
							style.value,
							() => {
								PC.hLength = style.hLength;
								billMod();
							});
					}
				}
			}
			*/

			// Length
			option = options.addOption(`${hasWig ? "Choose a longer or shorter wig" : "Cut or lengthen hair"}`, "hLength", PC);
			if (PC.hLength > 0) {
				for (const style of App.Medicine.Modification.hLength) {
					if (
						(style.hasOwnProperty("requirements") && !style.requirements(PC)) ||
						(style.hLength && style.hLength > PC.hLength)
					) {
						continue;
					}
					option.addValue(style.title, style.hLength);
				}
				if (!PC.bald && PC.hLength < 150) {
					option.addValue("Apply extensions", PC.hLength + 10);
				}
			} else {
				option.addValue("Apply hair growth stimulating treatment", 1);
			}

			option.showTextBox();

			// Maintain
			if (!hasWig) {
				options.addOption(`Keep your hair this length`, "haircuts", PC)
					.addValue("Enable", 1).on()
					.addValue("Disable", 0).off();
			}
		}

		el.append(options.render());
		return el;
	}

	function bodyHair() {
		const el = new DocumentFragment();
		const options = new App.UI.OptionsGroup();
		let option;
		let r = [];
		App.UI.DOM.appendNewElement("h3", el, "Body hair");

		// Eyebrows
		if (PC.eyebrowHStyle !== "bald") {
			// Describe and change color
			if (PC.eyebrowHStyle === "shaved" || PC.eyebrowHStyle === "bald") {
				r.push(`Your eyebrows would be ${PC.eyebrowHColor} if you had any.`);
			} else {
				r.push(`You have ${PC.eyebrowHColor} eyebrows.`);
			}

			option = options.addOption(r.join(" "), "eyebrowHColor", PC);
			if (PC.eyebrowHColor !== PC.hColor) {
				option.addValue("Match your hair", PC.hColor);
			}
			option.addValueList(makeAList(App.Medicine.Modification.Color.Primary.map(color => color.value)));
			option.pulldown();

			// Style
			option = options.addOption(`Style your ${PC.eyebrowHStyle} eyebrows`, "eyebrowHStyle", PC).showTextBox();
			for (const fullness of App.Medicine.Modification.eyebrowStyles) {
				option.addValue(capFirstChar(fullness), fullness);
			}
			option.pulldown();

			// Fullness
			option = options.addOption(`Adjust your ${PC.eyebrowFullness} eyebrows' fullness`, "eyebrowFullness", PC);
			for (const fullness of App.Medicine.Modification.eyebrowFullness) {
				option.addValue(capFirstChar(fullness), fullness);
			}
			option.pulldown();
		} else {
			options.addComment(`You have no eyebrows.`);
		}

		// Pubic hair
		const pubertyAge = Math.min(PC.pubertyAgeXX, PC.pubertyAgeXY);
		r = [];
		const hasPubes = (PC.pubicHStyle !== "bald" && PC.pubicHStyle !== "hairless" && PC.physicalAge >= pubertyAge - 1);
		const hasPitHair = (PC.underArmHStyle !== "bald" && PC.underArmHStyle !== "hairless" && PC.physicalAge >= pubertyAge - 1);
		if (hasPubes) {
			if (PC.physicalAge < pubertyAge) {
				r.push(`You've got a growing patch of wispy ${PC.pubicHColor} pubic hair.`);
			} else if (PC.pubicHStyle === "in a strip") {
				r.push(`You have a shaved strip of ${PC.pubicHColor} pubic hair.`);
			} else if (PC.pubicHStyle === "waxed") {
				r.push(`Your pubes would be ${PC.pubicHColor} if you stopped waxing them.`);
			} else {
				r.push(`Your ${PC.pubicHStyle} pubic hair is ${PC.pubicHColor}.`);
			}
		} else {
			r.push(`Your groin ${!hasPitHair ? "and underarms are" : "is"} completely hairless.`);
		}
		option = options.addOption(r.join(" "), "pubicHColor", PC);
		if (hasPubes) {
			if (PC.pubicHColor !== PC.hColor) {
				option.addValue("Match the curtains", PC.hColor);
			}
			option.addValueList(makeAList(App.Medicine.Modification.Color.Primary.map(color => color.value)))
				.pulldown();
			// Style
			option = options.addOption(`Style your pubic hair`, "pubicHStyle", PC);
			for (const fullness of App.Medicine.Modification.pubicStyles) {
				option.addValue(capFirstChar(fullness), fullness);
			}
			option.pulldown();
		}

		// Armpit hair
		r = [];
		if (hasPitHair) {
			r.push(`Your`);
			if (PC.physicalAge < pubertyAge) {
				r.push(`wispy underarm hair`);
			} else {
				r.push(`${PC.underArmHStyle} underarm hair`);
			}
			if (PC.underArmHStyle === "waxed") {
				r.push(`would be ${PC.underArmHColor} if left unwaxed.`);
			} else {
				r.push(`is ${PC.underArmHColor}.`);
			}
		} else if (hasPubes) {
			r.push(`Your underarms are completely hairless.`);
		}
		option = options.addOption(r.join(" "), "underArmHColor", PC);
		if (hasPitHair) {
			if (PC.underArmHColor !== PC.hColor) {
				option.addValue("Match the hair", PC.hColor);
			}
			option.addValueList(makeAList(App.Medicine.Modification.Color.Primary.map(color => color.value)))
				.pulldown();
			// Style
			option = options.addOption(`Style your armpit hair`, "underArmHStyle", PC);
			for (const fullness of App.Medicine.Modification.armpitStyles) {
				option.addValue(capFirstChar(fullness), fullness);
			}
			option.pulldown();
		}

		el.append(options.render());
		return el;
	}

	function tail() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Tail");
		const options = new App.UI.OptionsGroup();
		let title;
		let option;
		const filtered = App.Medicine.Modification.Color.Primary.filter(c => ![PC.tailColor].includes(c.value));

		title = `Your tail is ${PC.tailColor}${PC.tailEffect === "none" ? `` : ` with ${PC.tailEffect}`}.`;
		App.UI.DOM.appendNewElement("div", el, title);

		if (PC.earT !== "none" && (PC.earTColor !== PC.tailColor || PC.earTEffect !== PC.tailEffect)) {
			options.addCustomOption("")
				.addButton("Match your ears", () => {
					PC.tailColor = PC.earTColor;
					PC.tailEffect = PC.earTEffect;
					App.UI.reload();
				});
		} else if (PC.tailColor !== PC.hColor) {
			options.addCustomOption("")
				.addButton("Match your hair", () => {
					PC.tailColor = PC.hColor;
					PC.tailEffect = PC.hEffect;
					App.UI.reload();
				});
		}

		if (PC.tailEffect !== "none" || PC.tailEffectColor !== "none") {
			options.addCustomOption("")
				.addButton("Remove effects", () => {
					PC.tailEffect = "none";
					PC.tailEffectColor = "none";
					App.UI.reload();
				});
		}

		option = options.addOption("Fur Color", "tailColor", PC);
		for (const color of App.Medicine.Modification.Color.Primary) {
			option.addValue(capFirstChar(color.value), color.value);
		}
		option.pulldown();

		option = options.addOption("Effect", "tailEffect", PC);
		for (const color of App.Medicine.Modification.Color.Effect) {
			option.addValue(capFirstChar(color.value), `${PC.tailEffectColor} ${color.value}`);
		}
		option.pulldown();

		option = options.addOption("Effect color", "tailEffectColor", PC);
		for (const color of filtered) {
			option.addValue(capFirstChar(color.value), color.value);
		}
		option.pulldown();

		el.append(options.render());

		return el;
	}

	function appendages() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Appendages");
		const options = new App.UI.OptionsGroup();
		let title;
		let option;
		const filtered = App.Medicine.Modification.Color.Primary.filter(c => ![PC.appendagesColor].includes(c.value));

		title = `Your appendages are ${PC.appendagesColor}${PC.appendagesEffect === "none" ? `` : ` with ${PC.appendagesEffect}`}.`;
		App.UI.DOM.appendNewElement("div", el, title);

		if (PC.appendagesEffect !== "none" || PC.appendagesEffectColor !== "none") {
			options.addCustomOption("")
				.addButton("Remove effects", () => {
					PC.appendagesEffect = "none";
					PC.appendagesEffectColor = "none";
					App.UI.reload();
				});
		}

		option = options.addOption("Fur Color", "appendagesColor", PC);
		for (const color of App.Medicine.Modification.Color.Primary) {
			option.addValue(capFirstChar(color.value), color.value);
		}
		option.pulldown();

		option = options.addOption("Effect color", "appendagesEffectColor", PC);
		for (const color of filtered) {
			option.addValue(capFirstChar(color.value), color.value);
		}
		option.pulldown();

		option = options.addOption("Effect", "appendagesEffect", PC);
		for (const color of App.Medicine.Modification.Color.Effect) {
			option.addValue(capFirstChar(color.value), `${PC.appendagesEffectColor} ${color.value}`);
		}
		option.pulldown();

		el.append(options.render());

		return el;
	}

	function skin() {
		const el = new DocumentFragment();
		let option;
		App.UI.DOM.appendNewElement("h3", el, "Skin");
		const options = new App.UI.OptionsGroup();
		let comment = [];

		if (!isMovable(PC)) {
			App.UI.DOM.appendNewElement("div", el, `You can't do anything more as you are too large to leave your bed.`, "scene-intro");
		} else {
			App.UI.DOM.appendNewElement("div", el, `You can use the auto salon to modify yourself further.`, "skin-intro");

			option = options.addOption(`You have ${PC.skin} skin.`, "skin", PC);
			if (App.Medicine.Modification.dyedSkins.includes(PC.skin)) {
				option.addValue("Remove coloring", PC.origSkin, billMod);
			} else if ((PC.skin === "sun tanned") || (PC.skin === "spray tanned")) {
				option.addValue("Remove tanning", PC.origSkin, billMod);
			}

			if (!App.Medicine.Modification.dyedSkins.includes(PC.skin)) {
				if (PC.skin !== "sun tanned") {
					if (skinToneLevel(PC.skin) < 6) {
						comment.push(`Your skin is so light in color that any attempt at natural tanning is likely to damage your skin.`);
					} else if ((skinToneLevel(PC.skin) > 20)) {
						comment.push(`Your skin is so dark in color that any attempt at natural tanning is not likely to appear on your skin.`);
					} else {
						option.addValue("Sun tan", "sun tanned", billMod);
					}
				}
				if (PC.skin !== "spray tanned") {
					option.addValue("Spray tan", "sun tanned", billMod);
				}
				option.addComment(comment.join(" "));
			}

			option = options.addOption(`Dye or paint`, "skin", PC).showTextBox();
			for (const dye of App.Medicine.Modification.dyedSkins) {
				option.addValue(capFirstChar(dye), dye, billMod);
			}
			option.pulldown();

			if (PC.markings === "beauty mark") {
				option = options.addOption(`You have a prominent mole on your face`, "markings", PC)
					.addValue("Remove it", "none", billMod);
				if (PC.face > 40) {
					option.addComment(`Your face is so attractive that the mole qualifies as a beauty mark and enhances your allure.`);
				} else if (PC.face < -10) {
					option.addComment(`It isn't very attractive.`);
				} else {
					option.addComment(`It makes you stand out more, but that's all.`);
				}
			}
			if (PC.markings === "birthmark") {
				option = options.addOption(`You have a large visible birthmark`, "markings", PC)
					.addValue("Bleach it", "none", billMod);
				if (PC.prestige > 0) {
					option.addComment(`It makes you look unique among your peers.`);
				}
			}
		}
		el.append(options.render());

		return el;
	}

	function billMod() {
		cashX(forceNeg(V.modCost), "PCcosmetics", PC);
	}

	function makeAList(iterable) {
		return Array.from(iterable, (k => [capFirstChar(k), k]));
	}
};


code:
App.UI.Player.personalAttention = function() {
	const frag = new DocumentFragment();

	App.UI.DOM.appendNewElement("h1", frag, `Personal Attention`);

	V.nextButton = "Back to Main";
	V.nextLink = "Main";

	// set up div for refreshing options
	const refreshDiv = App.UI.DOM.appendNewElement("div", frag, content());

	return frag;

	function content() {
		const frag = new DocumentFragment();

		// ensure only the maximum number of slaves are being trained
		if (V.personalAttention.slaves && V.personalAttention.slaves.length > (V.PC.skill.slaving >= 100 ? 2 : 1)) {
			V.personalAttention.slaves.shift();
		}

		frag.append(
			focus(),
			proclamations(),
			skills(),
			attention(),
			slaves(),
		);

		return frag;
	}

	function focus() {
		const div = App.UI.DOM.makeElement("div", null, ['margin-bottom']);
		const text = [];
		const links = [];
		App.UI.DOM.appendNewElement("h2", div, `Focus`);

		if (!onBedRest(V.PC, true)) {
			text.push(`This week you are`);
		} else {
			text.push(`After you finish your mandatory bed rest, you are`);
		}

		if (V.personalAttention.task === PersonalAttention.TRAINING) {
			const r = [];
			for (const slave of V.personalAttention.slaves) {
				slave.objective = slave.objective || getRegimen(getSlave(slave.ID));
				r.push(`<span class="slave-name">${SlaveFullName(getSlave(slave.ID))}</span> to <span class="bold">${App.PersonalAttention.getText(slave.objective, getSlave(slave.ID))}</span>`);
			}
			text.push(`training ${toSentence(r)}.`);
		} else {
			switch (V.personalAttention.task) {
				case PersonalAttention.WHORING:
					if (isPCCareerInCategory("escort")) {
						text.push(`using your body to make some side cash.`);
					} else {
						text.push(`selling your body for much-needed cash.`);
					}
					break;
				case PersonalAttention.MAID:
					text.push(`using your housekeeping skills to help reduce upkeep costs on your home.`);
					break;
				case PersonalAttention.BUSINESS:
					text.push(`using your business acumen to earn some extra money.`);
					break;
				case PersonalAttention.IMAGE:
					text.push(`going to improve your public image.`);
					break;
				case PersonalAttention.SMUGGLING:
					text.push(`going to try to make some easy (but dirty) money.`);
					break;
				case PersonalAttention.SURVEY:
					text.push(`surveying ${V.arcologies[0].name}'s defenses in person.`);
					break;
				case PersonalAttention.DEVELOPMENT:
					text.push(`going to try to help raise ${V.arcologies[0].name}'s prosperity by contributing your talents to a local development project.`);
					break;
				case PersonalAttention.TECH:
					text.push(`contracting out your hacking skills to earn some extra cash.`);
					break;
				case PersonalAttention.FIGHT:
					text.push(`fighting for money.`);
					break;
				case PersonalAttention.SUPPORTHG:
					text.push(`going to support ${S.HeadGirl.slaveName} in ${getPronouns(S.HeadGirl).his} day-to-day routine.`);
					break;
				case PersonalAttention.SEX:
					text.push(`going to have as much sex as you possibly can.`);
					break;
				case PersonalAttention.RELAX:
					text.push(`doing nothing and enjoying your time off.`);
					break;
				case PersonalAttention.STUDY:
					text.push(`working on your general education.`);
					break;
				case PersonalAttention.GED:
					text.push(`striving to complete your general education.`);
					break;
				case PersonalAttention.EDUCATION:
					text.push(`working on furthering your education.`);
					break;
				case PersonalAttention.TEST:
					text.push(`proving yourself in order to earn an accredited diploma.`);
					break;
				case PersonalAttention.TRADE:
					text.push(`training in trade.`);
					break;
				case PersonalAttention.WAR:
					text.push(`training in warfare.`);
					break;
				case PersonalAttention.SLAVING:
					text.push(`training in slaving.`);
					break;
				case PersonalAttention.ENGINEERING:
					text.push(`training in engineering.`);
					break;
				case PersonalAttention.MEDICINE:
					text.push(`training in medicine.`);
					break;
				case PersonalAttention.HACKING:
					text.push(`training in hacking.`);
					break;
				case PersonalAttention.PROCLAMATION:
					if (V.SecExp.proclamation.type === "security") {
						text.push(`issuing a new proclamation about the security of ${V.arcologies[0].name}.`);
					} else {
						text.push(`issuing a new proclamation about crime in ${V.arcologies[0].name}.`);
					}
					break;
				default:
					throw new Error(`Unknown V.personalAttention.task value of '${V.personalAttention.task}'. Please report this.`);
			}
		}

		App.Events.addNode(div, text);

		addFocusLinks(links,
			{
				link: `Focus on "connecting"`,
				value: PersonalAttention.WHORING,
				prereqs: [
					() => isPCCareerInCategory("escort"),
				],
			},
			{
				link: `Sell your body for some quick ¤`,
				value: PersonalAttention.WHORING,
				prereqs: [
					() => !isPCCareerInCategory("escort"),
					() => V.cash < -5000
				],
			},
			{
				link: `Maintain your home`,
				value: PersonalAttention.MAID,
				prereqs: [
					() => isPCCareerInCategory("servant"),
				],
			},
			/*
			{
				link: `Get an education`,
				value: PersonalAttention.STUDY,
				prereqs: [
					() => V.PC.intelligenceImplant < 10,
				],
			},
			{
				link: `Take a GED exam`,
				value: PersonalAttention.GED,
				prereqs: [
					() => (V.PC.intelligenceImplant === 10 || V.PC.intelligence >= 50),
					() => V.cash >= 5000,
				],
			},
			{
				link: `Further your education`,
				value: PersonalAttention.EDUCATION,
				prereqs: [
					() => V.PC.intelligenceImplant < 20,
					() => V.cash >= 10000,
				],
			},
			{
				link: `Finish your education`,
				value: PersonalAttention.TEST,
				prereqs: [
					() => V.PC.intelligenceImplant === 20,
					() => V.cash >= 50000,
				],
			},
			*/
			{
				link: `Focus on business`,
				value: PersonalAttention.BUSINESS,
				prereqs: [
					() => ((!isPCCareerInCategory("escort") && !isPCCareerInCategory("servant")) || V.PC.skill.trading > 50),
				],
			},
			{
				link: `Help people "pass" things around`,
				value: PersonalAttention.SMUGGLING,
				prereqs: [
					() => isPCCareerInCategory("gang"),
				],
			},
			/*
			{
				link: `Improve your public image`,
				value: PersonalAttention.IMAGE,
				prereqs: [
					() => isMovable(V.PC),
					() => !isTrapped(V.PC),
				],
			},
			*/
			{
				link: `Survey your arcology's defenses in person`,
				value: PersonalAttention.SURVEY,
				prereqs: [
					() => V.PC.skill.warfare > 25,
				],
			},
			{
				link: `Contribute to a local development project`,
				value: PersonalAttention.DEVELOPMENT,
				prereqs: [
					() => V.PC.skill.engineering > 25,
					() => V.arcologies[0].prosperity + 1 * (1 + Math.ceil(V.PC.skill.engineering / 100)) < V.AProsperityCap,
				],
			},
			{
				link: `Sell your intrusion services`,
				value: PersonalAttention.TECH,
				prereqs: [
					() => V.PC.skill.hacking > 25,
				],
			},
			/*
			{
				link: `Bet on your fists`,
				value: PersonalAttention.FIGHT,
				prereqs: [
					() => V.PC.skill.combat > 20,
					() => canWalk(V.PC),
					() => hasAnyArms(V.PC),
				],
			},
			*/
			{
				link: `Support your Head Girl`,
				value: PersonalAttention.SUPPORTHG,
				prereqs: [
					() => !!S.HeadGirl,
				],
			},
			{
				link: `Focus on sex`,
				value: PersonalAttention.SEX,
			},
			{
				link: `Take a break`,
				value: PersonalAttention.RELAX,
			},
		);

		App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(links), ['margin-left']);

		return div;

		/**
		 * @param {Array<HTMLSpanElement|HTMLAnchorElement>} links
		 * @param {{link: string, value: PersonalAttention, prereqs?: Array<function():boolean>}[]} args
		 */
		function addFocusLinks(links, ...args) {
			args.forEach(link => {
				if (!link.prereqs || link.prereqs.every(prereq => prereq())) {
					if (V.personalAttention.task === link.value) {
						links.push(App.UI.DOM.disabledLink(link.link, [
							`Currently selected.`,
						]));
					} else {
						links.push(App.UI.DOM.link(link.link, () => {
							V.personalAttention = {task: link.value};

							refresh();
						}));
					}
				}
			});
		}
	}

	function proclamations() {
		const div = App.UI.DOM.makeElement("div", null, ['margin-bottom']);

		const links = [];

		if (V.secExpEnabled) {
			App.UI.DOM.appendNewElement("h2", div, `Proclamations`);

			if (V.SecExp.proclamation.cooldown === 0) {
				const options = new App.UI.OptionsGroup();
				const option = options.addOption(null, "currency", V.SecExp.proclamation);

				App.UI.DOM.appendNewElement("div", div, `You can dedicate the week to issuing a proclamation, a powerful tool that will have an immediate noticeable effect on the arcology.`, ['note']);

				App.Events.addNode(div, [
					`You are using <span class="bold">${V.SecExp.proclamation.currency || 'cash'}.</span>`,
				]);

				if (V.SecExp.core.authority >= 2000) {
					option.addValue(`Authority`, 'authority');
				}

				if (V.rep >= 4000) {
					option.addValue(`Reputation`, 'reputation');
				}

				option.addValue(`Cash`, 'cash');

				App.UI.DOM.appendNewElement("div", div, options.render(), ['margin-left']);

				if (V.SecExp.core.authority >= 2000 ||
					V.rep >= 4000 ||
					V.cash >= 8000) {
					if (V.personalAttention.task === PersonalAttention.PROCLAMATION) {
						if (V.SecExp.proclamation.type === "security") {
							links.push(
								App.UI.DOM.disabledLink(`Issue a proclamation about security`, [
									`Currently selected.`
								]),

								App.UI.DOM.link(`Issue a proclamation about crime`, () => {
									V.personalAttention.task = PersonalAttention.PROCLAMATION;
									V.SecExp.proclamation.type = "crime";

									refresh();
								},
								null, ``, `You will use your ${V.SecExp.proclamation.currency === "authority"
									? `control over the arcology`
									: V.SecExp.proclamation.currency === "reputation"
										? `great influence`
										: `vast financial means`
								} to force the arrest of suspected citizens without passing through the normal legal procedures.`),
							);
						} else {
							links.push(
								App.UI.DOM.link(`Issue a proclamation about security`, () => {
									V.personalAttention.task = PersonalAttention.PROCLAMATION;
									V.SecExp.proclamation.type = "security";

									refresh();
								},
								null, ``, `You will use your ${V.SecExp.proclamation.currency === "authority"
									? `control over the arcology`
									: V.SecExp.proclamation.currency === "reputation"
										? `great influence`
										: `vast financial means`
								} to force citizens to give up on sensitive information for the good of the arcology.`),

								App.UI.DOM.disabledLink(`Issue a proclamation about crime`, [
									`Currently selected.`
								]),
							);
						}
					} else {
						links.push(
							App.UI.DOM.link(`Issue a proclamation about security`, () => {
								V.personalAttention.task = PersonalAttention.PROCLAMATION;
								V.SecExp.proclamation.type = "security";

								refresh();
							},
							null, ``, `You will use your ${V.SecExp.proclamation.currency === "authority"
								? `control over the arcology`
								: V.SecExp.proclamation.currency === "reputation"
									? `great influence`
									: `vast financial means`
							} to force citizens to give up on sensitive information for the good of the arcology.`),

							App.UI.DOM.link(`Issue a proclamation about crime`, () => {
								V.personalAttention.task = PersonalAttention.PROCLAMATION;
								V.SecExp.proclamation.type = "crime";

								refresh();
							},
							null, ``, `You will use your ${V.SecExp.proclamation.currency === "authority"
								? `control over the arcology`
								: V.SecExp.proclamation.currency === "reputation"
									? `great influence`
									: `vast financial means`
							} to force the arrest of suspected citizens without passing through the normal legal procedures.`),
						);
					}

					App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(links), ['margin-left']);
				}
			} else {
				App.UI.DOM.appendNewElement("div", div, `It's too early to issue another proclamation. Another will be available in ${numberWithPluralOne(V.SecExp.proclamation.cooldown, "week")}.`, ['note']);
			}
		}

		return div;
	}

	function skills() {
		const div = App.UI.DOM.makeElement("div", null, ['margin-bottom']);
		const cost = 10000 * V.AgeEffectOnTrainerPricingPC;

		App.UI.DOM.appendNewElement("h2", div, `Skills`);

		div.append(addSkills(
			{
				skill: "trading",
				text: `trader`,
				link: {
					value: PersonalAttention.TRADE,
					text: `Hire a merchant to train you in commerce`,
				},
			},
			{
				skill: "warfare",
				text: `tactician`,
				link: {
					value: PersonalAttention.WAR,
					text: `Hire a mercenary to train you in warfare`,
				},
			},
			{
				skill: "slaving",
				text: `slaver`,
				link: {
					value: PersonalAttention.SLAVING,
					text: `Hire a slaver to train you in slaving`,
				},
			},
			{
				skill: "engineering",
				text: `arcology engineer`,
				link: {
					value: PersonalAttention.ENGINEERING,
					text: `Hire an engineer to train you in arcology engineering`,
				},
			},
			{
				skill: "medicine",
				text: `surgeon`,
				link: {
					value: PersonalAttention.MEDICINE,
					text: `Hire a doctor to train you in medicine`,
				},
			},
			{
				skill: "hacking",
				text: `hacker`,
				link: {
					value: PersonalAttention.HACKING,
					text: `Hire a specialist to train you in hacking`,
				},
			},
		));

		return div;

		/**
		 * @param {Array<{skill: string, text: string, link: {value: PersonalAttention, text: string}}>} args
		 * @returns {HTMLDivElement}
		 */
		function addSkills(...args) {
			const div = document.createElement("div");
			const skillMaster = [];

			args.forEach(skill => {
				div.append(
					addSkill(skill.skill, skill.text, {
						value: skill.link.value,
						text: skill.link.text,
					}),
				);
			});

			if (skillMaster.length > 0) {
				div.append(`You are a master ${toSentence(skillMaster)}.`);
			}
			return div;

			/**
			 * @param {string} skill
			 * @param {string} text
			 * @param {{value: PersonalAttention, text: string}} link
			 */
			function addSkill(skill, text, link) {
				const div = document.createElement("div");

				if (V.PC.skill[skill] >= 100) {
					skillMaster.push(`${text}`);
				} else {
					if (V.PC.skill[skill] > 60) {
						div.append(`You are an expert ${text}.`);
					} else if (V.PC.skill.hacking > 30) {
						div.append(`You have some skill as ${addA(text)}.`);
					} else if (V.PC.skill.hacking > 10) {
						div.append(`You have basic knowledge as ${addA(text)}.`);
					} else {
						div.append(`You have no knowledge as ${addA(text)}.`);
					}

					addLink(div, {
						value: link.value,
						link: link.text,
					});
				}

				return div;

				/**
				 * @param {HTMLDivElement} div
				 * @param {{value: PersonalAttention, link: string}} skill
				 */
				function addLink(div, skill) {
					if (V.personalAttention.task === skill.value) {
						App.UI.DOM.appendNewElement("div", div, App.UI.DOM.disabledLink(skill.link, [
							`Currently selected.`,
						]), ['margin-left']);
					} else {
						App.UI.DOM.appendNewElement("div", div, App.UI.DOM.link(skill.link, () => {
							V.personalAttention = {task: skill.value};

							refresh();
						}, null, '', `Training will cost ${cashFormat(cost)} per week.`), ['margin-left']);
					}
				}
			}
		}
	}

	function attention() {
		/**
		 * @param {number} index
		 * @param {string} text
		 * @param {string} target
		 * @param {string} [tooltip]
		 * @returns {HTMLSpanElement|HTMLAnchorElement}
		 */
		function attentionLink(index, text, target, tooltip) {
			if (V.personalAttention.slaves[index].objective === target) {
				return App.UI.DOM.disabledLink(text, [
					'Currently selected.'
				]);
			} else {
				return App.UI.DOM.link(text, () => {
					V.personalAttention.slaves[index].objective = target;
					refresh();
				}, null, '', tooltip);
			}
		}

		const frag = new DocumentFragment();
		App.UI.DOM.appendNewElement("h2", frag, `Slaves`);
		if (!V.personalAttention.slaves || V.personalAttention.slaves.length === 0) {
			frag.append(`You have not selected a slave for your personal attention.`);
		} else {
			for (let i = 0; i < V.personalAttention.slaves.length; i++) {
				const div = App.UI.DOM.appendNewElement("div", frag, null, ['margin-bottom', 'card']);
				const slave = getSlave(V.personalAttention.slaves[i].ID);
				const regimen = V.personalAttention.slaves[i].objective;
				div.id = `attention-${slave.ID}`;	// used for scrolling back up after selecting a slave
				if (V.personalAttention.slaves.filter(s => s.ID === slave.ID).length > 1) {
					V.personalAttention.slaves = [];
				}

				const {He, him, his} = getPronouns(slave);
				let text = [];
				let links = [];
				V.personalAttention.slaves[i].objective = V.personalAttention.slaves[i].objective || getRegimen(slave);
				text.push(
					`You will give`,
					App.UI.DOM.referenceSlaveWithPreview(slave, SlaveFullName(slave)),
					`your personal attention this week to ${App.PersonalAttention.getText(V.personalAttention.slaves[i].objective, slave)}.`,
					App.UI.DOM.link(`Stop`, () => {
						if (V.personalAttention.slaves.length === 1) {
							V.personalAttention = {task: PersonalAttention.SEX};
						} else {
							V.personalAttention.slaves.deleteAt(i);
						}
						refresh();
					}),
				);
				App.Events.addNode(div, text);
				App.UI.DOM.appendNewElement("h3", div, `Change training objective`);

				// HORNY
				if (isHorny(V.PC) && !isPlayerFrigid()) {
					if (canDoAnal(slave) || canDoVaginal(slave)) {
						if (canAchieveErection(V.PC) || V.PC.clit >= 3) {
							links.push(attentionLink(i, `Sate your lust in ${him}`, "ravish"));
						} else if (V.PC.vagina >= 0) {
							links.push(attentionLink(i, `Sate your lust in ${him} using a double-ended dildo`, "ravish"));
						} else if (V.PC.dick > 0) {
							links.push(App.UI.DOM.disabledLink(`Sate your lust in ${him}`, [
								`You want to ravish ${him}, but you can't get it up. What bullshit!`
							]));
						}
					}
					if (V.PC.vagina >= 0) {
						if (canPenetrate(slave)) {
							links.push(attentionLink(i, `Get fucked until you're satisfied`, "ravished"));
						} else if (!canAchieveErection(slave) && slave.dick > 0) {
							links.push(App.UI.DOM.disabledLink(`Get fucked until you're satisfied`, [
								`You need dick, but ${his} is completely worthless!`
							]));
						}
					}
				}

				// Devotion
				App.Events.addNode(div, [
					`Current devotion: `,
					devotionText(slave),
				]);
				if (slave.devotion <= 20 && slave.trust >= -20) {
					links.push(
						attentionLink(i, `Break ${his} will`, "break will"),
						attentionLink(i, `Use enhanced breaking techniques`, "harshly break will"),
					);
				} else {
					links.push(attentionLink(i, `Build ${his} devotion`, "build devotion"));
				}

				// Sexuality
				if (!slave.fetishKnown || !slave.attrKnown) {
					links.push(attentionLink(i, `Explore ${his} sexuality and fetishes`, "explore sexuality"));
				} else {
					links.push(App.UI.DOM.disabledLink(`Explore ${his} sexuality and fetishes`, [
						`You already understand ${his} sexuality.`
					]));
				}

				// Education
				const whoreSkillCap = isPCCareerInCategory("escort") ? 60 : 30;
				if (slave.devotion <= 20 && slave.trust >= -20) {
					links.push(App.UI.DOM.disabledLink(`Teach ${him}`, [
						`${He}'s too disobedient to learn sex skills`
					]));
				} else if (slave.skill.anal >= 100 &&
					slave.skill.oral >= 100 &&
					slave.skill.penetrative >= 30 &&
					slave.skill.whoring >= whoreSkillCap &&
					slave.skill.entertainment >= whoreSkillCap) {
					if (slave.skill.vaginal >= 100) {
						links.push(App.UI.DOM.disabledLink(`Teach ${him}`, [
							`${He} knows all the skills you can teach`
						]));
					} else if (slave.vagina < 0) {
						links.push(App.UI.DOM.disabledLink(`Teach ${him}`, [
							`${He} knows all the skills you can teach a slave without a vagina`
						]));
					} else {
						links.push(attentionLink(i, `Teach ${him}`, "learn skills"));
					}
				} else {
					links.push(attentionLink(i, `Teach ${him}`, "learn skills"));
				}
				if (canWalk(V.PC) && hasBothArms(V.PC) && V.PC.skill.combat > 30) {
					if (slave.assignment === Job.BODYGUARD || (slave.devotion + slave.trust >= 190 && canWalk(slave) && hasBothArms(slave))) {
						if (slave.skill.combat < V.PC.skill.combat - (130 - (V.PC.intelligence + V.PC.intelligenceImplant))) {
							links.push(attentionLink(i, `Train ${him} to fight`, "combat training"));
						} else if (slave.skill.combat > 30) {
							links.push(attentionLink(i, `Spar with ${him}`, "spar"));
						}
					}
				}

				// Health
				App.Events.addNode(div, [
					`Current health: `,
					healthText(slave),
				]);
				links.push(attentionLink(i, `Care for ${him}`, "health"));
				if (!FutureSocieties.isActive('FSPaternalist')) {
					if (getPersonalAttention(null, "torture")) {
						links.push(App.UI.DOM.disabledLink(`Make ${him} suffer`, [
							`You can only torture one slave at a time`
						]));
					} else {
						links.push(attentionLink(i, `Make ${him} suffer`, "torture"));
					}
				}
				App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(links), ['margin-left']);
				links = [];

				// Behavioral Flaws
				if (slave.behavioralFlaw !== "none") {
					const existingQuirk = slave.behavioralQuirk !== 'none' ? slave.behavioralQuirk : '';
					const r = new SpacedTextAccumulator(div);

					div.append(
						App.UI.DOM.makeElement("h3", `Correct a behavioral flaw`)
					);

					if (existingQuirk) {
						r.push("Current behavioral quirk:", App.UI.DOM.makeElement("span", `${capFirstChar(existingQuirk)}.`, ['green']));
					}
					r.push("Current behavioral flaw:", App.UI.DOM.makeElement("span", `${capFirstChar(slave.behavioralFlaw)}.`, ["red"]));
					r.toNode("div");
					links.push(attentionLink(i, `Remove ${his} behavioral flaw`, "fix behavioral flaw"));

					if (slave.devotion < -20) {
						links.push(App.UI.DOM.disabledLink(`Soften ${his} behavioral flaw`, [
							`${He} must be broken before ${his} flaws can be softened`
						]));
					} else {
						links.push(attentionLink(i, `Soften ${his} behavioral flaw`, "soften behavioral flaw", existingQuirk && `Will replace existing quirk.`));
					}

					App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(links), ['margin-left']);
					links = [];
				}

				div.append(
					App.UI.DOM.makeElement("h3", `Induce a behavioral flaw`),
				);
				addLinks(
					slave,
					regimen,
					links,
					i,
					{
						quirk: "confident",
						flaw: "arrogant",
						type: "induce arrogance",
						link: `Arrogance`,
					},
					{
						quirk: "cutting",
						flaw: "bitchy",
						type: "induce bitchiness",
						link: `Bitchiness`,
					},
					{
						quirk: "funny",
						flaw: "odd",
						type: "induce odd behavior",
						link: `Odd behavior`,
					},
					{
						quirk: "adores women",
						flaw: "hates men",
						type: "induce hatred of men",
						link: `Hatred of men`,
					},
					{
						quirk: "adores men",
						flaw: "hates women",
						type: "induce hatred of women",
						link: `Hatred of women`,
					},
					{
						quirk: "fitness",
						flaw: "gluttonous",
						type: "induce gluttony",
						link: `Gluttony`,
					},
					{
						quirk: "insecure",
						flaw: "anorexic",
						type: "induce anorexia",
						link: `Anorexia`,
					},
					{
						quirk: "sinful",
						flaw: "devout",
						type: "induce religious devotion",
						link: `Religious devotion`,
					},
					{
						quirk: "advocate",
						flaw: "liberated",
						type: "induce liberation",
						link: `Liberation`,
					},
				);
				App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(links), ['margin-left']);
				links = [];

				// Sexual Flaws
				if (slave.sexualFlaw !== "none") {
					const isParaphilia = App.Data.misc.paraphiliaList.includes(slave.sexualFlaw);
					const existingQuirk = slave.sexualQuirk !== 'none' ? slave.sexualQuirk : '';
					const r = new SpacedTextAccumulator(div);

					div.append(
						App.UI.DOM.makeElement("h3", `Correct a sexual flaw`)
					);

					if (existingQuirk) {
						r.push("Current sexual quirk:", App.UI.DOM.makeElement("span", `${capFirstChar(existingQuirk)}.`, ['green']));
					}
					r.push("Current sexual flaw:", App.UI.DOM.makeElement("span", `${capFirstChar(slave.sexualFlaw)}.`, isParaphilia ? ["yellow"] : ["red"]));
					r.toNode("div");
					links.push(attentionLink(i, `Remove ${his} ${isParaphilia ? "paraphilia" : "sexual flaw"}`, "fix sexual flaw"));

					if (slave.devotion < -20) {
						links.push(App.UI.DOM.disabledLink(`Soften ${his} sexual flaw`, [
							`${He} must be broken before ${his} flaws can be softened`
						]));
					} else if (isParaphilia) {
						links.push(App.UI.DOM.disabledLink(`Soften ${his} sexual flaw`, [
							`Paraphilias cannot be softened`
						]));
					} else {
						links.push(attentionLink(i, `Soften ${his} sexual flaw`, "soften sexual flaw", existingQuirk && `Will replace existing quirk.`));
					}

					App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(links), ['margin-left']);
					links = [];
				}

				div.append(
					App.UI.DOM.makeElement("h3", `Induce a sexual flaw`),
				);
				addLinks(
					slave,
					regimen,
					links,
					i,
					{
						quirk: "gagfuck queen",
						flaw: "hates oral",
						type: "induce hatred of oral",
						link: `Hatred of oral`,
					},
					{
						quirk: "painal queen",
						flaw: "hates anal",
						type: "induce hatred of anal",
						link: `Hatred of anal`,
					},
					{
						quirk: "strugglefuck queen",
						flaw: "hates penetration",
						type: "induce hatred of penetration",
						link: `Hatred of penetration`,
					},
					{
						quirk: "tease",
						flaw: "shamefast",
						type: "induce shame",
						link: `Shame`,
					},
					{
						quirk: "romantic",
						flaw: "idealistic",
						type: "induce sexual idealism",
						link: `Sexual idealism`,
					},
					{
						quirk: "perverted",
						flaw: "repressed",
						type: "induce sexual repression",
						link: `Sexual repression`,
					},
					{
						quirk: "caring",
						flaw: "apathetic",
						type: "induce sexual apathy",
						link: `Sexual apathy`,
					},
					{
						quirk: "unflinching",
						flaw: "crude",
						type: "induce crudity",
						link: `Crudity`,
					},
					{
						quirk: "size queen",
						flaw: "judgemental",
						type: "induce judgement",
						link: `Judgment`,
					},
				);
				App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(links), ['margin-left']);
				links = [];

				// Paraphilias
				App.UI.DOM.appendNewElement("h3", div, `Induce a paraphilia`);
				if (slave.fetishStrength >= 95) {
					addLinks(
						slave,
						regimen,
						links,
						i,
						{
							fetish: "cumslut",
							flaw: "cum addict",
							type: "induce cum addiction",
							link: `Cum addiction`,
						},
						{
							fetish: "buttslut",
							flaw: "anal addict",
							type: "induce anal addiction",
							link: `Anal addiction`,
						},
						{
							fetish: "humiliation",
							flaw: "attention whore",
							type: "induce attention whoring",
							link: `Attention whoring`,
						},
						{
							fetish: "boobs",
							flaw: "breast growth",
							type: "induce breast growth obsession",
							link: `Breast growth obsession`,
						},
						{
							fetish: "dom",
							flaw: "abusive",
							type: "induce abusiveness",
							link: `Abusiveness`,
						},
						{
							fetish: "sadist",
							flaw: "malicious",
							type: "induce maliciousness",
							link: `Maliciousness`,
						},
						{
							fetish: "masochist",
							flaw: "self hating",
							type: "induce self hatred",
							link: `Self hatred`,
						},
						{
							fetish: "submissive",
							flaw: "neglectful",
							type: "induce sexual self neglect",
							link: `Sexual self neglect`,
						},
						{
							fetish: "pregnancy",
							flaw: "breeder",
							type: "induce breeding obsession",
							link: `Breeding obsession`,
						},
					);
					App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(links), ['margin-left']);
					links = [];
				} else {
					App.UI.DOM.appendNewElement("div", div, `Paraphilias can be induced from a strong fetish.`, ['note', 'margin-left']);
				}
			}
		}

		return frag;

		/**
		 * @typedef Link
		 *
		 * @property {FC.SexualFlaw|FC.BehavioralFlaw} flaw
		 * @property {FC.SexualQuirk|FC.BehavioralQuirk} [quirk]
		 * @property {FC.Fetish} [fetish]
		 * @property {string} type
		 * @property {string} link
		 */

		/**
		 *
		 * @param {App.Entity.SlaveState} slave
		 * @param {string} regimen
		 * @param {Array<HTMLAnchorElement|HTMLSpanElement>} links
		 * @param {number} index
		 * @param {Link[]} args
		 */
		function addLinks(slave, regimen, links, index, ...args) {
			args.forEach(link => {
				if (slave.sexualFlaw !== link.flaw &&
					slave.behavioralFlaw !== link.flaw &&
					(!link.quirk || slave.sexualQuirk !== link.quirk &&
						slave.behavioralQuirk !== link.quirk) &&
					(!link.fetish || slave.fetish === link.fetish)) {
					if (regimen === link.type) {
						links.push(App.UI.DOM.disabledLink(link.link, [
							`Currently selected.`,
						]));
					} else {
						links.push(App.UI.DOM.link(link.link, () => {
							V.personalAttention.slaves[index].objective = link.type;

							refresh();
						}, null, '', `Inducing flaws is difficult and bad for slaves' obedience.`));
					}
				}
			});
		}
	}

	function slaves() {
		const div = App.UI.DOM.makeElement("div", null, ['margin-bottom']);
		App.UI.DOM.appendNewElement("h3", div, `Select a slave to train`);
		if (V.PC.skill.slaving >= 100) {
			App.Events.addNode(div, [
				`Your <span class="skill player">slaving experience</span> allows you to divide your attention between more than one slave each week, with slightly reduced efficiency.`
			], "div", ['note', 'margin-left']);
		}

		div.append(App.UI.SlaveList.slaveSelectionList(
			s => (assignmentVisible(s) || App.Entity.facilities.masterSuite.isHosted(s)) && s.fuckdoll === 0,
			s => App.UI.DOM.link(SlaveFullName(s),
				(id) => { selectSlave(id); }, [s.ID]
			),
		));

		return div;
	}

	function refresh() {
		App.UI.DOM.replace(refreshDiv, content());
	}

	function selectSlave(id) {
		if (!V.personalAttention.slaves) { // first PA target
			V.personalAttention = {
				task: PersonalAttention.TRAINING,
				slaves: [{
					ID: id,
					objective: getRegimen(getSlave(id)),
				}]
			};
		} else {
			const paIndex = V.personalAttention.slaves.findIndex(s => s.ID === id);
			if (paIndex === -1) { // not already a PA target; add
				V.personalAttention.task = PersonalAttention.TRAINING; // make sure
				V.personalAttention.slaves.push({
					ID: id,
					objective: getRegimen(getSlave(id)),
				});
			} else { // already a PA target; remove
				V.personalAttention.slaves.deleteAt(paIndex);
				if (V.personalAttention.slaves.length === 0) {
					V.personalAttention = {task: PersonalAttention.SEX};
				}
			}
		}

		refresh();

		if (V.personalAttention.task === PersonalAttention.TRAINING && V.personalAttention.slaves.includes(id)) {
			document.getElementById(`attention-${id}`).scrollIntoView();
		}
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @returns {DocumentFragment}
	 */
	function devotionText(slave) {
		const frag = new DocumentFragment();

		App.UI.SlaveSummaryImpl.helpers.makeRatedStyledSpan(frag, App.Data.SlaveSummary.long.mental.devotion, slave.devotion, 100, true);

		return frag;
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @returns {DocumentFragment}
	 */
	function healthText(slave) {
		const frag = new DocumentFragment();

		App.UI.SlaveSummaryImpl.helpers.makeRatedStyledSpan(frag, App.Data.SlaveSummary.long.health.health, slave.health.condition, 100, true);

		return frag;
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @returns {string}
	 */
	function getRegimen(slave) {
		if (slave.health.condition < -20) {
			return `health`;
		} else if (slave.behavioralFlaw !== "none") {
			if (slave.devotion < -20) {
				return `fix behavioral flaw`;
			} else {
				return `soften behavioral flaw`;
			}
		} else if (slave.sexualFlaw !== "none") {
			if (slave.devotion < -20 || App.Data.misc.paraphiliaList.includes(slave.sexualFlaw)) {
				return `fix sexual flaw`;
			} else {
				return `soften sexual flaw`;
			}
		} else if (!slave.fetishKnown) {
			return `explore sexuality`;
		} else if (slave.devotion <= 20 && slave.trust >= -20) {
			return `break will`;
		}
		return `build devotion`;
	}
};

repo_name: Arkerthan/fc

license: mit


code:
/*
receiver - What is the state of the network box.
	-1: PC has not been given a welcome packet. (default)
	0: PC has been given a welcome packet but has not installed it.
	1+: box is installed.
channel[numberAsString] - how many times it has been viewed.
channel.last - program viewed last week.
pcViewership.count - How many weeks since the PC last watched FCTV.
pcViewership.frequency - How often should the PC watch FCTV.
	-1: Never.
	1: at least once a week.
	2: least every two weeks.
	4: at least once a month. (default)
remote - Does the PC have a FCTV branded remote (also used to trigger a slave acquisition event).
weekEnabled - The week FCTV was installed.
*/
globalThis.FCTV = (function() {
	return {
		channels: channels,
		initChannels: initChannels,
		channelCount: channelCount,
		FinalTouches: FinalTouches,
	};

	/** Produce an array of numbers representing all available channels in App.Data.FCTV.channels
	 * @returns {number[]}
	 */
	function channels() {
		return Object.keys(App.Data.FCTV.channels).map(chStr => parseInt(chStr, 10));
	}

	function initChannels() {
		const channelList = FCTV.channels();
		for (let i = 0; i < channelList.length; i++) {
			V.FCTV.channel[num(channelList[i], true)] = 0;
		}
	}

	/**
	 * checks value of selected channel in relation to an arbitrary number. Have we watched it more/less/same than i.
	 * @param {number} selectedChannel
	 * @param {number} i
	 * @param {string} operation eq/gt/lt: equals/greater than/less than.
	 * @returns {boolean}
	 */
	function channelCount(selectedChannel, i, operation = 'eq') {
		if (operation === 'eq') {
			if (V.FCTV.channel[num(selectedChannel, true)] === i) {
				return true;
			}
		} else if (operation === 'gt') {
			if (V.FCTV.channel[num(selectedChannel, true)] > i) {
				return true;
			}
		} else if (operation === 'lt') {
			if (V.FCTV.channel[num(selectedChannel, true)] < i) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Applies some universal changes to FCTV slaves
	 * @param {App.Entity.SlaveState} slave
	 * @returns {App.Entity.SlaveState}
	 */
	function FinalTouches(slave) {
		slave.pubertyXX = 1;
		slave.career = "a slave";
		slave.origin = "You purchased $him from FCTV's Home Slave Shopping stream channel.";
		setHealth(slave, 80, 0, 0, 0, 0);
		return slave;
	}
})();


code:
// @ts-nocheck
App.Update.FCTV = function() {
	function convert(a, b) {
		return V.FCTVreceiver ? a : b;
	}

	function initChannels() {
		const channelList = FCTV.channels();
		for (let i = 0; i < channelList.length; i++) {
			const channel = num(channelList[i], true);
			const currentChannel = 'show' + capFirstChar(channel);
			V.FCTV.channel[channel] = convert(V[currentChannel], V.FCTV.channel[channel]) || 0;
		}
	}

	V.FCTV = V.FCTV || {};
	V.FCTV.receiver = V.FCTV.receiver > -1 ? V.FCTV.receiver : -1;
	if (convert && V.receiverAvailable) {
		V.FCTV.receiver = V.FCTVreceiver > 0 ? V.FCTVreceiver : 0;
	}
	V.FCTV.pcViewership = V.FCTV.pcViewership || {};
	V.FCTV.channel = V.FCTV.channel || {};
	initChannels();
	V.FCTV.channel.last = convert(V.lastShow, V.FCTV.channel.last) || -1;

	V.FCTV.pcViewership.count = convert(V.FCTVcount, V.FCTV.pcViewership.count) || 0;
	V.FCTV.pcViewership.frequency = convert(V.FCTVrate, V.FCTV.pcViewership.frequency) || 4;
	V.FCTV.remote = convert(V.FCTVremote, V.FCTV.remote) || 0;

	if (V.FCTVreceiver && !V.FCTV.weekEnabled) {
		V.FCTV.weekEnabled = V.receiverAvailable > 1 ? V.receiverAvailable : 0;
	}

	if (V.FCTV.channel.hasOwnProperty("selected")) {
		delete V.FCTV.channel.selected;
	}
};


code:
button.selectedslave { background-color: #b84; }
table.slave-genetic-details { width: 100%; margin: 0 10px; border: 1px solid white; table-layout: fixed; font-size: 85%; border-collapse: separate; padding: 2px; }
table.slave-genetic-details tr { padding: 0 3px; }
table.slave-genetic-details td { vertical-align: top; border: 1px solid black; padding: 0 3px; }
table.slave-genetic-details th { vertical-align: top; background-color: rgba(127, 127, 127, 0.2); padding: 0 3px; }
table.slave-genetic-details td.editor { border: 1px solid #777; }
table.slave-genetic-details td.editor:hover { border: 1px solid #8cf; transition-duration: 0.3s; }
table.slave-genetic-details td.editor input { width: calc(100% + 8px); height: 100%; min-width: 0; margin: 0 -4px; padding: 4px 5px; box-sizing: border-box; }
table.slave-genetic-details td.editor select { width: calc(100% + 8px); height: 100%; min-width: 0; margin: 0 -4px; padding: 3px 5px; box-sizing: border-box; border: 1px solid #777; }
table.slave-genetic-details td.editor select:hover { border: 1px solid #8cf; transition-duration: 0.3s; }


code:
App.UI.editGenetics = function() {
	const node = new DocumentFragment();
	const cupCat = new Categorizer(
		[0, 'flat'],
		[300, 'A-cup'],
		[400, 'B-cup'],
		[500, 'C-cup'],
		[650, 'D-cup'],
		[800, 'DD-cup'],
		[1000, 'F-cup'],
		[1200, 'G-cup'],
		[1400, 'H-cup'],
		[1600, 'I-cup'],
		[1800, 'J-cup'],
		[2050, 'K-cup'],
		[2300, 'L-cup'],
		[2600, 'M-cup'],
		[2900, 'N-cup'],
		[3250, 'O-cup'],
		[3600, 'P-cup'],
		[3950, 'Q-cup'],
		[4300, 'R-cup'],
		[4700, 'S-cup'],
		[5100, 'massive']
	);
	const faceCat = new Categorizer(
		[-Infinity, 'very ugly'],
		[-95, 'ugly'],
		[-40, 'unattractive'],
		[-10, 'attractive'],
		[11, 'very pretty'],
		[41, 'gorgeous'],
		[96, 'mind blowing']
	);
	const lipsCat = new Categorizer(
		[0, 'thin'],
		[11, 'normal'],
		[21, 'pretty'],
		[41, 'plush'],
		[71, 'huge'],
		[96, 'facepussy']
	);
	const hormonalCat = new Categorizer(
		[-Infinity, 'heavy male'],
		[-49, 'male'],
		[-24, 'natural'],
		[25, 'female'],
		[50, 'heavy female']
	);
	const intelligenceCat = new Categorizer(
		[-Infinity, 'borderline retarded'],
		[-95, 'very slow'],
		[-50, 'slow'],
		[-15, 'average'],
		[16, 'smart'],
		[51, 'very smart'],
		[96, 'brilliant']
	);
	function birthFullName(s) {
		let r = [];
		if (V.surnameOrder !== 1) {
			if (["Cambodian", "Chinese", "Ancient Chinese Revivalist", "Hungarian", "Japanese", "Edo Revivalist", "Korean", "Mongolian", "Taiwanese", "Vietnamese"].includes(s.nationality)) {
				if (s.birthSurname || s.surname) {
					r.push(s.birthSurname || s.surname);
				}
				r.push(App.UI.DOM.makeElement("span", (s.birthName || s.name), ["bold", "underline"]));
			} else {
				r.push(App.UI.DOM.makeElement("span", (s.birthName || s.name), ["bold", "underline"]));
				if (s.birthSurname || s.surname) {
					r.push(s.birthSurname || s.surname);
				}
			}
		} else {
			r.push(App.UI.DOM.makeElement("span", (s.birthName || s.name), ["bold", "underline"]));
			if (s.birthSurname || s.surname) {
				r.push(s.birthSurname || s.surname);
			}
		}
		return App.Events.makeNode(r);
	}
	function currentFullName(s) {
		let r = [];
		if (V.surnameOrder !== 1) {
			if (["Cambodian", "Chinese", "Ancient Chinese Revivalist", "Hungarian", "Japanese", "Edo Revivalist", "Korean", "Mongolian", "Taiwanese", "Vietnamese"].includes(s.nationality)) {
				if (s.slaveSurname) {
					r.push(s.slaveSurname);
				}
				r.push(App.UI.DOM.makeElement("span", s.slaveName, ["bold", "underline"]));
			} else {
				r.push(App.UI.DOM.makeElement("span", s.slaveName, ["bold", "underline"]));
				if (s.slaveSurname) {
					r.push(s.slaveSurname);
				}
			}
		} else {
			r.push(App.UI.DOM.makeElement("span", s.slaveName, ["bold", "underline"]));
			if (s.slaveSurname) {
				r.push(s.slaveSurname);
			}
		}
		return App.Events.makeNode(r);
	}
	function nameButton(s) {
		const button = App.UI.DOM.makeElement("button", currentFullName(s), "slavepicker");
		button.setAttribute("data-slave", s.ID);
		button.style.width = "100%";
		return button;
	}
	function geneDetailsFunction(s) {
		function makeRow() {
			row = App.UI.DOM.appendNewElement("tr", tbody);
		}
		function makeCell(text, colSpan) {
			let cell = App.UI.DOM.appendNewElement("td", row, text);
			if (colSpan) {
				cell.colSpan = colSpan;
			}
		}
		function makeHeader(text) {
			App.UI.DOM.appendNewElement("th", row, text);
		}

		function makeHr() {
			makeRow();
			row.append(document.createElement("td"));
			cell = App.UI.DOM.appendNewElement("td", row, App.UI.DOM.makeElement("hr"));
			cell.colSpan = 6;
			row.append(document.createElement("td"));
		}

		const table = document.createElement("table");
		table.classList.add("slave-genetic-details");
		const tbody = App.UI.DOM.appendNewElement("tbody", table);

		let row = document.createElement("tr");
		let cell;
		makeRow();

		makeHeader(`Given name(s)`);
		makeCell(s.slaveName, 2);

		makeHeader(`Family name`);
		makeCell(s.slaveSurname || '', 2);

		makeHeader(`Karyotype`);
		cell = App.UI.DOM.appendNewElement("td", row, `${s.genes} (${toSex(s.genes)})`, ["editor", "choice-editor"]);
		cell.setAttribute("data-param", "genes");
		cell.setAttribute("data-choices", "XX, XY");

		makeRow();

		makeHeader(`Nationality`);
		cell = App.UI.DOM.appendNewElement("td", row, s.nationality, ["editor", "string-editor"]);
		cell.colSpan = 2;
		cell.setAttribute("data-param", "nationality");

		makeHeader(`Ethnicity`);
		cell = App.UI.DOM.appendNewElement("td", row, s.origRace, ["editor", "choice-editor"]);
		cell.colSpan = 2;
		cell.setAttribute("data-param", "origRace");
		cell.setAttribute("data-choices", "amerindian, asian, black, indo-aryan, latina, malay, middle eastern, mixed race, pacific islander, semitic, southern european, catgirl, white");

		makeHeader(`Career`);
		cell = App.UI.DOM.appendNewElement("td", row, s.career ? s.career : '', ["note"]);

		makeRow();

		makeHeader(`Father`);
		makeCell(parentFullName(s.father), 2);

		makeHeader(`Mother`);
		makeCell(parentFullName(s.mother), 2);

		makeHeader(`Age`);
		makeCell(ageDesc(s));

		if (s.birthName !== s.slaveName || s.birthSurname !== s.slaveSurname) {
			makeRow();

			cell = App.UI.DOM.appendNewElement("td", row, App.Events.makeNode([`Born as`, birthFullName(s)]), ["note"]);
			cell.colSpan = 8;
		}

		makeHr();

		makeRow();

		makeHeader(`Skin`);
		cell = App.UI.DOM.appendNewElement("td", row, s.origSkin, ["editor", "string-editor"]);
		cell.setAttribute("data-param", "origSkin");

		makeHeader(`Skin markings`);
		cell = App.UI.DOM.appendNewElement("td", row, s.origSkin, ["editor", "choice-editor"]);
		cell.setAttribute("data-param", "markings");
		cell.setAttribute("data-choices", "none, beauty mark, birthmark, freckles, heavily freckled");

		makeHeader(`Eyes`);
		cell = App.UI.DOM.appendNewElement("td", row, s.eye.origColor, ["editor", "string-editor"]);
		cell.setAttribute("data-param", "eye.origColor");

		makeHeader(`Hair`);
		cell = App.UI.DOM.appendNewElement("td", row, s.origHColor, ["editor", "string-editor"]);
		cell.setAttribute("data-param", "origHColor");

		makeRow();

		makeHeader(`Height`);
		cell = App.UI.DOM.appendNewElement("td", row, `${s.height} cm (${toFeet(s.height)})`, ["editor", "number-editor"]);
		cell.setAttribute("data-param", "height");
		cell.setAttribute("data-min", "50");
		cell.setAttribute("data-max", "250");

		makeHeader(`Weight`);
		cell = App.UI.DOM.appendNewElement("td", row, percent(s.weight), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "weight");

		makeHeader(`Muscles`);
		cell = App.UI.DOM.appendNewElement("td", row, percent(s.muscles), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "muscles");

		cell = App.UI.DOM.appendNewElement("td", row);
		cell.colSpan = 2;

		makeRow();

		makeHeader(`Shoulders`);
		cell = App.UI.DOM.appendNewElement("td", row, shouldersDesc(s.shoulders), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "shoulders");
		cell.setAttribute("data-min", "-2");
		cell.setAttribute("data-max", "2");

		makeHeader(`Waist`);
		cell = App.UI.DOM.appendNewElement("td", row, percent(s.waist), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "waist");

		makeHeader(`Hips`);
		cell = App.UI.DOM.appendNewElement("td", row, hipsDesc(s.hips), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "hips");
		cell.setAttribute("data-min", "-2");
		cell.setAttribute("data-max", "3");

		makeHeader(`Rear`);
		cell = App.UI.DOM.appendNewElement("td", row, rearDesc(s.butt), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "butt");
		cell.setAttribute("data-min", "0");
		cell.setAttribute("data-max", "20");

		makeRow();

		makeHeader(`Breasts`);
		cell = App.UI.DOM.appendNewElement("td", row, `${s.boobs} cc (${cupCat.cat(s.boobs)})`, ["editor", "number-editor"]);
		cell.setAttribute("data-param", "boobs");
		cell.setAttribute("data-min", "0");
		cell.setAttribute("data-max", "100000");

		makeHeader(`Breast shape`);
		cell = App.UI.DOM.appendNewElement("td", row, s.boobShape, ["editor", "choice-editor"]);
		cell.setAttribute("data-param", "boobShape");
		cell.setAttribute("data-choices", "normal, perky, saggy, torpedo-shaped, downward-facing, wide-set");

		makeHeader(`Nipples`);
		cell = App.UI.DOM.appendNewElement("td", row, s.nipples, ["editor", "choice-editor"]);
		cell.setAttribute("data-param", "nipples");
		cell.setAttribute("data-choices", "huge, puffy, inverted, tiny, cute, partially inverted");

		makeHeader(`Areolae`);
		cell = App.UI.DOM.appendNewElement("td", row, areolaeDesc(s.areolae), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "areolae");
		cell.setAttribute("data-min", "0");
		cell.setAttribute("data-max", "5");

		makeRow();

		makeHeader(`Face`);
		cell = App.UI.DOM.appendNewElement("td", row, `${faceCat.cat(s.face)} (${percent(s.face)})`, ["editor", "number-editor"]);
		cell.setAttribute("data-param", "face");

		makeHeader(`Lips`);
		cell = App.UI.DOM.appendNewElement("td", row, `${lipsCat.cat(s.lips)} (${s.lips})`, ["editor", "number-editor"]);
		cell.setAttribute("data-param", "lips");
		cell.setAttribute("data-min", "0");
		cell.setAttribute("data-max", "100");

		makeHeader(`Teeth`);
		cell = App.UI.DOM.appendNewElement("td", row, s.teeth, ["editor", "choice-editor"]);
		cell.setAttribute("data-param", "teeth");
		cell.setAttribute("data-choices", "normal, crooked, straightening braces, cosmetic braces, removable, pointy, baby, mixed");

		makeHeader(`Voice`);
		cell = App.UI.DOM.appendNewElement("td", row, voiceDesc(s.voice), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "voice");
		cell.setAttribute("data-min", "0");
		cell.setAttribute("data-max", "3");

		makeHr();

		if (s.vagina >= 0) {
			makeRow();

			makeHeader(`Labia`);
			cell = App.UI.DOM.appendNewElement("td", row, labiaDesc(s.labia), ["editor", "number-editor"]);
			cell.setAttribute("data-param", "labia");
			cell.setAttribute("data-min", "0");
			cell.setAttribute("data-max", "3");

			makeHeader(`Clitoris`);
			cell = App.UI.DOM.appendNewElement("td", row, clitorisDesc(s.clit), ["editor", "number-editor"]);
			cell.setAttribute("data-param", "clit");
			cell.setAttribute("data-min", "0");
			cell.setAttribute("data-max", "5");

			makeHeader(`Ovaries`);
			cell = App.UI.DOM.appendNewElement("td", row,
				s.ovaries
					? App.Events.makeNode(['working', App.UI.DOM.makeElement("span", "\u2714", "green")])
					: App.Events.makeNode(['broken', App.UI.DOM.makeElement("span", "\u2718", "red")])
			);
			cell.setAttribute("data-param", "ovaries");
			cell.setAttribute("data-min", "0");
			cell.setAttribute("data-max", "5");

			makeHeader(`Ovum type`);
			makeCell(s.eggType);
		}
		if (s.dick > 0 || s.balls > 0) {
			makeRow();

			makeHeader(`Penis`);
			cell = App.UI.DOM.appendNewElement("td", row, penisDesc(s.dick), ["editor", "number-editor"]);
			cell.setAttribute("data-param", "dick");
			cell.setAttribute("data-min", "0");
			cell.setAttribute("data-max", "11");

			makeHeader(`Testes`);
			cell = App.UI.DOM.appendNewElement("td", row, testesDesc(s.balls), ["editor", "number-editor"]);
			cell.setAttribute("data-param", "balls");
			cell.setAttribute("data-min", "0");
			cell.setAttribute("data-max", "10");

			makeHeader(`Prostate`);
			cell = App.UI.DOM.appendNewElement("td", row,
				s.prostate
					? App.Events.makeNode(['working', App.UI.DOM.makeElement("span", "\u2714", "green")])
					: App.Events.makeNode(['broken', App.UI.DOM.makeElement("span", "\u2718", "red")]),
			);
			cell.setAttribute("data-param", "prostate");

			makeHeader(`Sperm type`);
			makeCell(s.ballType);
		}

		makeRow();

		makeHeader(`Hormonal`);
		cell = App.UI.DOM.appendNewElement("td", row, `${hormonalCat.cat(s.hormoneBalance)} (${s.hormoneBalance})`, ["editor", "number-editor"]);
		cell.setAttribute("data-param", "hormoneBalance");

		makeHeader(`Anal area`);
		cell = App.UI.DOM.appendNewElement("td", row, s.analArea, ["editor", "number-editor"]);
		cell.setAttribute("data-param", "analArea");
		cell.setAttribute("data-min", "0");
		cell.setAttribute("data-max", "3");

		cell = App.UI.DOM.appendNewElement("td", row);
		cell.colSpan = 4;

		makeHr();

		makeRow();

		makeHeader(`Intelligence`);
		cell = App.UI.DOM.appendNewElement("td", row, intelligenceCat.cat(s.intelligence), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "intelligence");
		cell.setAttribute("data-min", "-100");
		cell.setAttribute("data-max", "100");

		makeHeader(`Behavioral`);
		cell = App.UI.DOM.appendNewElement("td", row, s.behavioralFlaw !== 'none' ? s.behavioralFlaw : s.behavioralQuirk);

		makeHeader(`Sexual`);
		cell = App.UI.DOM.appendNewElement("td", row, s.sexualFlaw !== 'none' ? s.sexualFlaw : s.sexualQuirk);

		cell = App.UI.DOM.appendNewElement("td", row);
		cell.colSpan = 2;

		if (s.chem > 0 || s.addict > 0) {
			makeRow();
			makeCell(chemicalsDesc(s), 8);
		}
		return table;
	}
	function percent(n) {
		if (n > 0) { return '+' + Number(n).toFixed(0) + '%'; }
		if (n === 0) { return '\u00B10%'; }
		return Number(n).toFixed(0) + '%';
	}
	function toFeet(n) {
		let realFeet = Number(n) / 30.48;
		let feet = Math.floor(realFeet);
		let inches = Math.round((realFeet - feet) * 12);
		return feet + "\u2032" + inches + '\u2033';
	}
	function toSex(karyotype) {
		return {
			XX: 'female',
			XY: 'male',
			X: 'Turner syndrome female',
			X0: 'Turner syndrome female',
			XYY: 'XYY syndrome male',
			XXY: 'Klinefelter syndrome male',
			XXX: 'triple X syndrome female'
		}[String(karyotype).toUpperCase()] || 'unknown/not viable';
	}
	function ageDesc(slave) {
		let age = slave.actualAge + slave.birthWeek/52.0;
		let years = Math.floor(age);
		let months = Math.floor((age - years) * 12);
		return (years > 0 ? years + 'y ' + months + 'm' : months + 'm');
	}
	function shouldersDesc(s) {
		return ({
			'-2': 'very narrow',
			'-1': 'narrow',
			'0': 'feminine',
			'1': 'broad',
			'2': 'very broad'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function hipsDesc(s) {
		return ({
			'-2': 'very narrow',
			'-1': 'narrow',
			'0': 'normal',
			'1': 'wide',
			'2': 'very wide',
			'3': 'inhumanly wide'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function rearDesc(s) {
		return ({
			'0': 'flat',
			'1': 'small',
			'2': 'plump',
			'3': 'big',
			'4': 'huge',
			'5': 'enormous',
			'6': 'gigantic',
			'7': 'ridiculous',
			'8': 'immense',
			'9': 'immense',
			'10': 'immense',
			'11': 'inhuman',
			'12': 'inhuman',
			'13': 'inhuman',
			'14': 'inhuman',
			'15': 'inhuman',
			'16': 'inhuman',
			'17': 'inhuman',
			'18': 'inhuman',
			'19': 'inhuman',
			'20': 'inhuman'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function areolaeDesc(s) {
		return ({
			'0': 'normal',
			'1': 'large',
			'2': 'unusually wide',
			'3': 'huge',
			'4': 'heart shaped',
			'5': 'star shaped'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function voiceDesc(s) {
		return ({
			'0': 'mute',
			'1': 'deep',
			'2': 'feminine',
			'3': 'high'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function labiaDesc(s) {
		return ({
			'0': 'minimal',
			'1': 'big',
			'2': 'huge',
			'3': 'huge dangling'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function clitorisDesc(s) {
		return ({
			'0': 'normal',
			'1': 'large',
			'2': 'huge',
			'3': 'enormous',
			'4': 'penis-like',
			'5': 'like a massive penis'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function penisDesc(s) {
		return ({
			'0': 'none',
			'1': 'tiny',
			'2': 'little',
			'3': 'normal',
			'4': 'big',
			'5': 'huge',
			'6': 'gigantic',
			'7': 'gigantic',
			'8': 'titanic',
			'9': 'absurd',
			'10': 'inhuman',
			'11': 'hypertrophied'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function testesDesc(s) {
		return ({
			'0': 'none',
			'1': 'vestigial',
			'2': 'small',
			'3': 'average',
			'4': 'big',
			'5': 'huge',
			'6': 'enormous',
			'7': 'hypertrophied', '8': 'hypertrophied', '9': 'hypertrophied', '10': 'hypertrophied'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function chemicalsDesc(slave) {
		let res = [];
		if (slave.chem > 0) {
			res.push('Immune system damaged (' + slave.chem.toFixed(0) + ').');
		}
		if (slave.addict > 0) {
			res.push('Addicted to aphrodisiacs (' + slave.addict.toFixed(0) + ').');
		}
		return res.join(' ');
	}
	function parentFullName(id) {
		if (id === 0) {
			return App.UI.DOM.makeElement("span", "unknown", "note");
		}
		if (id === -1) {
			/* The PC */
			return birthFullName(V.PC) + ' (PC)';
		} else {
			let parent = V.genePool.find(function(s) {
				return s.ID === id;
			});
			return parent
				? birthFullName(parent)
				: App.Events.makeNode([
					App.UI.DOM.makeElement("span", "missing", "note"),
					`(${id})`
				]);
		}
	}

	let outerDiv = App.UI.DOM.appendNewElement("div", node);
	outerDiv.style.position = "relative";
	outerDiv.style.width = "100%";
	outerDiv.style.height = "100%";

	let innerDiv = App.UI.DOM.appendNewElement("div", outerDiv);
	innerDiv.id = "slaveList";
	innerDiv.style.width = "20em";
	innerDiv.style.float = "left";

	innerDiv = App.UI.DOM.appendNewElement("div", outerDiv);
	innerDiv.id = "geneDetails";
	innerDiv.style.position = "fixed";
	innerDiv.style.left = "0";
	innerDiv.style.right = "0";
	innerDiv.style.top = "0";
	innerDiv.style.bottom = "0";
	innerDiv.style.marginLeft = "40em";
	innerDiv.style.marginRight = "2.5em";
	innerDiv.style.marginTop = "2.5em";

	jQuery(function() {
		let slaveList = jQuery('#slaveList');
		_(V.genePool).sortBy(['birthName', 'birthSurname']).forEach(function(s) {
			slaveList.append(nameButton(s));
		});
		jQuery(document).off('.edit-genetics');
		jQuery(document).on('keyup.edit-genetics', function(e) {
			if (slaveList.is(':visible')) {
				let currentSelection = jQuery('button.selectedslave');
				switch (e.keyCode) {
					case 38: /* ArrowUp */
						if (currentSelection.length === 0) {
							slaveList.find('button').last().trigger('click');
						} else {
							currentSelection.prev().trigger('click');
						}
						break;
					case 40: /* ArrowDown */
						if (currentSelection.length === 0) {
							slaveList.find('button').first().trigger('click');
						} else {
							currentSelection.next().trigger('click');
						}
						break;
					default:
						/* do nothing */
						break;
				}
			}
		});
		let geneDetails = jQuery('#geneDetails');
		jQuery('.slavepicker').each(function() {
			let el = jQuery(this);
			let id = Number(el.attr('data-slave'));
			el.on('click', function() {
				jQuery('button.selectedslave').removeClass('selectedslave');
				el.addClass('selectedslave');
				let slave = V.genePool.find(function(s) { return s.ID === id; });
				geneDetails.html(geneDetailsFunction(slave));

				let numberEditorOpen = function() {
					let td = jQuery(this);
					td.off('click');
					let attr = String(td.attr('data-param'));
					let min = Number(td.attr('data-min')) || -100;
					let max = Number(td.attr('data-max')) || 100;
					let editableText = jQuery('<input type="number" min="' + min + '" max="' + max + '" />');
					editableText.val(Number(slave[attr]));
					editableText.keyup(function(e) {
						if (e.keyCode === 13) {
							slave[attr] = Number(jQuery(this).val());
							el.trigger('click');
						}
					});
					editableText.blur(function() {
						slave[attr] = Number(jQuery(this).val());
						el.trigger('click');
					});
					td.empty().append(editableText);
					editableText.focus();
				};
				geneDetails.find('td.number-editor').click(numberEditorOpen);
				let stringEditorOpen = function() {
					let td = jQuery(this);
					td.off('click');
					let attr = String(td.attr('data-param'));
					let editableText = jQuery('<input type="text" />');
					editableText.val(String(slave[attr]));
					editableText.keyup(function(e) {
						if (e.keyCode === 13) {
							slave[attr] = String(jQuery(this).val());
							el.trigger('click');
						}
					});
					editableText.blur(function() {
						slave[attr] = String(jQuery(this).val());
						el.trigger('click');
					});
					td.empty().append(editableText);
					editableText.focus();
				};
				geneDetails.find('td.string-editor').click(stringEditorOpen);
				let choiceEditorOpen = function() {
					let td = jQuery(this);
					td.off('click');
					let attr = String(td.attr('data-param'));
					let choices = String(td.attr('data-choices')).split(/,\s*/);
					let editableText = jQuery('<select>' + choices.map(ch => '<option>' + ch + '</option>').join('') + '</select>');
					editableText.val(String(slave[attr]));
					editableText.keyup(function(e) {
						if (e.keyCode === 13) {
							slave[attr] = String(jQuery(this).val());
						}
					});
					editableText.blur(function() {
						slave[attr] = String(jQuery(this).val());
					});
					td.empty().append(editableText);
					editableText.focus();
				};
				geneDetails.find('td.choice-editor').each(choiceEditorOpen);
			});
		});
	});

	return node;
};


code:
App.Interact.eliteBreedingExam = function(slave = null) {
	const frag = new DocumentFragment();
	const eliteAngered = V.failedElite > 100;
	const SlaveProfessionalismEstablished = V.arcologies[0].FSSlaveProfessionalism > 20;
	const cost = 5000;

	let r = [];

	if (!slave) {
		r.push(`A fee of <span class='yellowgreen'>${cashFormat(cost)}</span> will be deducted from your account for each slave be to examined.`);
	}
	r.push(
		`Updates will be posted periodically. It is your responsibility to keep up to date.`,
		`Failures will be sterilized. Please make sure your slave fits the following criteria before submitting them for testing. We shall not be held responsible for time wasted.`,
	);
	if (!slave) {
		r.push(`The slave must not be inside a Fuckdoll suit, infertile, or already pregnant.`);
	} else {
		r.push(`${slave.slaveName} is up for review:`);
	}
	App.Events.addNode(frag, r, "div");
	r = [];

	const table = App.UI.DOM.appendNewElement("table", frag, null, ["padded-columns"]);
	/** @type {[(slave: FC.SlaveState) => boolean, string][]} */
	const checks = [];

	checks.push(
		[
			slave => slave.intelligence + slave.intelligenceImplant >= (eliteAngered ? 95 : 50),
			`Must be ${eliteAngered ? 'highly intelligent' : 'very smart'}`
		],
		[
			slave => Beauty(slave) >= (eliteAngered ? 120 : 100),
			`Must be ${eliteAngered ? 'unbelievably' : ''} beautiful`
		],
		[
			slave => slave.face >= (eliteAngered ? 95 : 40),
			`Must have a ${eliteAngered ? 'breathtaking' : 'gorgeous'} face`
		],
		[
			slave => slave.chem <= 20,
			`Must not be overly impacted by drug use`
		],
		[
			slave => slave.addict === 0,
			`Must not be addicted to drugs`
		],
	);

	if (V.arcologies[0].FSMaturityPreferentialist > 20 || V.arcologies[0].FSYouthPreferentialist > 20) {
		const targetAge = 24;

		if (V.arcologies[0].FSMaturityPreferentialist > 20) {
			checks.push([
				slave => slave.physicalAge > targetAge,
				`Must be older than ${targetAge}`
			]);
		} else {
			checks.push([
				slave => slave.physicalAge <= targetAge,
				`Must be ${targetAge} or younger`
			]);
		}
	}

	if (V.arcologies[0].FSIntellectualDependency > 20) {
		checks.push([
			slave => slave.energy >= 96,
			`Must be sexually receptive at all times`
		]);
	} else if (V.arcologies[0].FSSlaveProfessionalism > 20) {
		checks.push(
			[
				slave => slave.skill.vaginal + adjustedPenSkill(slave) + slave.skill.anal + slave.skill.oral + slave.skill.entertainment + slave.skill.whoring >= (eliteAngered ? 500 : 400),
				`Must be capable of mastering skills`
			],
			[
				slave => slave.accent <= 1,
				`Must be capable of eloquent and accurate speech`
			],
		);
	}

	if (V.arcologies[0].FSPetiteAdmiration > 20 || V.arcologies[0].FSStatuesqueGlorification > 20) {
		const targetHeight = V.arcologies[0].FSPetiteAdmiration > 20 ? 150 : 186;
		if (V.arcologies[0].FSPetiteAdmiration > 20) {
			checks.push(
				[
					slave => (slave.height - (slave.heightImplant * 10)) < targetHeight,
					`Must naturally be very short (${targetHeight}cm)`
				],
				[
					slave => (slave.height - (slave.heightImplant * 10)) < Height.mean(slave) - 15,
					`Must naturally be very short for their age`
				],
			);
		} else if (V.arcologies[0].FSStatuesqueGlorification > 20) {
			checks.push([
				slave => (slave.height - (slave.heightImplant * 10)) > targetHeight,
				`Must naturally be very tall (${targetHeight}cm)`
			]);
		}
	}

	if (V.arcologies[0].FSSupremacist > 20 || V.arcologies[0].FSSubjugationist > 20) {
		const targetRace = V.arcologies[0].FSSupremacist > 20 ? V.arcologies[0].FSSupremacistRace : V.arcologies[0].FSSubjugationistRace;
		if (V.arcologies[0].FSSupremacist > 20) {
			checks.push([
				slave => slave.race === targetRace,
				`Must be ${targetRace}`
			]);
		} else {
			checks.push([
				slave => slave.race !== targetRace,
				`Must not be ${targetRace}`
			]);
		}
	}

	if (V.arcologies[0].FSGenderRadicalist > 20) {
		checks.push([
			slave => slave.dick >= 1 && slave.balls >= 1,
			`Must have a functional penis`
		]);
	} else if (V.arcologies[0].FSGenderFundamentalist > 20) {
		checks.push([
			slave => slave.dick === 0 && slave.balls === 0,
			`Must be physically female`
		]);
	}

	if (V.arcologies[0].FSPaternalist > 20) {
		checks.push([
			slave => slave.intelligenceImplant >= (SlaveProfessionalismEstablished ? 30 : 15),
			`Must be ${SlaveProfessionalismEstablished ? 'well' : ''} educated`
		]);
		checks.push([
			slave => slave.health.condition >= 60,
			`Must be in good health`
		]);
	}

	if (SlaveProfessionalismEstablished) {
		checks.push([
			slave => slave.intelligenceImplant >= 15,
			`Must be well educated`
		]);
	}

	if (V.arcologies[0].FSBodyPurist > 20) {
		checks.push([
			slave => slave.chem <= (eliteAngered ? 0 : 15),
			`Must have low carcinogen levels`
		]);
		checks.push([
			slave => slave.boobsImplant + slave.buttImplant + slave.lipsImplant + slave.hipsImplant + slave.shouldersImplant === 0 && slave.faceImplant <= 5 && slave.bellyImplant === -1,
			`Must be implant free`
		]);
	}

	if (V.arcologies[0].FSSlimnessEnthusiast > 20) {
		if (V.arcologies[0].FSHedonisticDecadence > 20) {
			checks.push([
				slave => slave.weight <= 30,
				`May be no larger than "plush"`
			]);
		} else {
			checks.push([
				slave => slave.weight <= 0,
				`Must be thin`
			]);
		}
		checks.push([
			slave => slave.butt <= 2,
			`Must have a trim rear`
		]);
		checks.push([
			slave => slave.boobs <= 400,
			`Must have a sleek chest`
		]);
	} else if (V.arcologies[0].FSAssetExpansionist > 20) {
		if (V.arcologies[0].FSTransformationFetishist > 20) {
			checks.push(
				[
					slave => slave.butt >= 6,
					`Must have a gigantic rear`
				],
				[
					slave => slave.boobs >= 1000,
					`Must be busty`
				],
			);
		} else {
			checks.push(
				[
					slave => slave.butt - slave.buttImplant >= 6,
					`Must have a naturally gigantic rear`
				],
				[
					slave => slave.boobs - slave.boobsImplant >= 1000,
					`Must be naturally busty`
				],
			);
		}
	}

	if (V.arcologies[0].FSPastoralist > 20) {
		checks.push([
			slave => slave.lactation === 1,
			`Must be lactating naturally`
		]);
	}
	if (V.arcologies[0].FSPhysicalIdealist > 20) {
		if (V.arcologies[0].FSPhysicalIdealistLaw === 1) {
			const musclesMin = 20;
			const musclesMax = 50;

			checks.push(
				[
					slave => slave.weight <= 30,
					`Must not be overweight`
				],
				[
					slave => slave.muscles.isBetween(musclesMin, musclesMax),
					`Must be fit, but not too muscular`
				],
			);
		} else {
			checks.push([
				slave => slave.muscles > 95,
				`Must be extremely muscular`
			]);
		}
	} else if (V.arcologies[0].FSHedonisticDecadence > 20) {
		const targetWeight = V.arcologies[0].FSSlimnessEnthusiast > 20 ? 10 : 95;

		if (V.arcologies[0].FSSlimnessEnthusiast > 20) {
			checks.push([
				slave => slave.weight > targetWeight,
				`Must be more than "trim"`
			]);
		} else {
			checks.push([
				slave => slave.weight > targetWeight,
				`Must be big, soft and fat`
			]);
		}
	}

	if (slave) {
		checks.forEach(check => {
			const tr = App.UI.DOM.appendNewElement("tr", table);

			App.UI.DOM.appendNewElement("td", tr, `• ${check[1]}`);

			if (!check[0](slave)) {
				App.UI.DOM.appendNewElement("td", tr, `FAILED`, ["red"]);
			} else {
				App.UI.DOM.appendNewElement("td", tr, `PASSED`, ["lime"]);
			}
		});

		if (checks.some(check => !check[0](slave))) {
			r.push(`The aforementioned slave has been deemed <span class="red">unsuitable</span> for breeding.`);

			slave.preg = -3;
		} else {
			r.push(`The aforementioned slave has been deemed <span class="green">worthy</span> of being used for breeding and has been marked as such. Please note the increased restrictions on breeding slaves.`);
			r.push(`They are not permitted for public use or anything that may harm their growing child. The child within them is considered a member of the Elite class and as such, any harm that comes to them will result in severe penalties to the breeder's owner. Development of the child will be closely monitored; should the fetus be identified as not of the owner's blood (or any other member of the Elite class), said owner shall face severe fines.`);

			slave.breedingMark = 1;
			slave.pregControl = "none";

			const job = App.Utils.jobForAssignment(slave.assignment);
			const consequences = [];
			if (slave.assignment === Job.BODYGUARD || (job && job.desc.publicSexUse)) {
				removeJob(slave, slave.assignment);
				consequences.push(`reassigned to <span class="green">rest</span>`);
			}
			if (V.pit) {
				if (V.pit.trainingIDs.includes(slave.ID)) {
					removeJob(slave, Job.ARENA);
					consequences.push(`<span class="yellow">removed</span> from ${V.pit.name}'s training roster`);
				}
				if (V.pit.fighterIDs.includes(slave.ID)) {
					removeJob(slave, Job.PIT);
					consequences.push(`<span class="yellow">removed</span> from ${V.pit.name}'s fighting roster`);
				}
			}
			if (consequences.length > 0) {
				App.Events.addNode(frag, r, "div");
				r = [];
				const {He} = getPronouns(slave);
				r.push(`${He} has been automatically ${toSentence(consequences)}.`);
			}
		}

		cashX(-cost, "capEx");
	} else {
		checks.forEach(check => {
			const tr = App.UI.DOM.appendNewElement("tr", table);

			App.UI.DOM.appendNewElement("td", tr, check[1]);
		});
	}

	// hack to circumvent default SugarCube styling
	table.style.marginLeft = "1em";

	App.Events.addNode(frag, r, "div");

	return frag;
};