<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
	<Meta name="ExplicitAutoJoints">true</Meta>
	<External>null</External>
	<External>nil</External>
	<Item class="Script" referent="RBX68305EDBE4AB24ED5F0B9B0544C262D6">
		<Properties>
			<bool name="Disabled">false</bool>
			<string name="Name">Nighty</string>
			<ProtectedString name="Source"><![CDATA[--[[
	Nighty — the Roblox Studio plugin of Nighty Design (https://nightkilleryt.com/tools/ui-designer)
	Copyright (c) 2026 NightyCode. All rights reserved.
	Free to use in Roblox Studio for your own games and projects; the UI you make with it is yours.
	Do not sell, re-upload, share or publish this plugin or a changed copy, and do not use its code anywhere else.
]]
--!strict

local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Selection = game:GetService("Selection")
local StudioService = game:GetService("StudioService")

local App = require(script.App)
local Brand = require(script.Brand)
local Copies = require(script.Copies)
local History = require(script.History)
local Settings = require(script.Settings)
local Uploader = require(script.Uploader)

if not RunService:IsEdit() then
	return
end

local WIDGET_ID = "NKD_UIDesigner"
local WINDOW_ID = WIDGET_ID .. "_Window"
local ICON = Brand.toolbarIcon or ""
local EXTENSIONS = { Brand.bundleExtension, table.unpack(Brand.legacyBundleExtensions) }

local function method(object: any, newName: string, oldName: string): ((...any) -> ...any)?
	local ok, found = pcall(function()
		return object[newName]
	end)
	if ok and type(found) == "function" then
		return found
	end
	local okOld, old = pcall(function()
		return object[oldName]
	end)
	if okOld and type(old) == "function" then
		return old
	end
	return nil
end

local toolbar = plugin:CreateToolbar(Brand.pluginName)
local button = toolbar:CreateButton(
	WIDGET_ID .. "_Open",
	"Import " .. Brand.productName .. " designs, update them in place, send UI back",
	ICON,
	Brand.pluginName
)
button.ClickableWhenViewportHidden = true

local info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Right, false, false, 320, 600, 240, 320)
local createWidget = method(plugin, "CreateDockWidgetPluginGuiAsync", "CreateDockWidgetPluginGui")
assert(createWidget, "This Studio can't make plugin windows.")
local widget: DockWidgetPluginGui = createWidget(plugin, WINDOW_ID, info)
widget.Title = Brand.pluginName
widget.Name = WIDGET_ID
widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling

local function pickFile(): (buffer?, string?)
	local prompt = method(StudioService, "PromptImportFileAsync", "PromptImportFile")
	if prompt == nil then
		error("This Studio can't open files from a plugin.")
	end
	local file = prompt(StudioService, EXTENSIONS)
	if file == nil then
		return nil, nil
	end
	local contents = file:GetBinaryContents()
	return buffer.fromstring(contents), file.Name
end

local function saveSelection(fileName: string): boolean
	local save = method(plugin, "PromptSaveSelectionAsync", "PromptSaveSelection")
	if save == nil then
		error("This Studio can't save files from a plugin.")
	end
	return save(plugin, fileName) == true
end

local app = App.new({
	host = widget,
	settings = Settings.new(Settings.pluginStorage(plugin)),
	uploader = Uploader.new(),
	recorder = History.studio,
	pickFile = pickFile,
	getSelection = function()
		return Selection:Get()
	end,
	setSelection = function(instances: { Instance })
		Selection:Set(instances)
	end,
	saveSelection = saveSelection,
	service = function(name: string): Instance
		return game:GetService(name)
	end,
	isRunning = function()
		return RunService:IsRunning()
	end,
	userId = Uploader.userId,
	userName = function(userId: number): string?
		return Players:GetNameFromUserIdAsync(userId)
	end,
	clock = os.clock,
	wait = function(seconds: number)
		task.wait(seconds)
	end,
	log = function(text: string)
		print(text)
	end,
	random = Random.new(),
	fetch = function(url: string): string
		return HttpService:GetAsync(url, true)
	end,
	now = os.time,
})
task.spawn(function()
	app:loadUserName()
end)

local stopCopies = function() end
pcall(function()
	stopCopies = Copies.watch(
		game:GetService("PluginGuiService"),
		widget,
		Brand.pluginVersion,
		Brand.pluginRelease,
		function(others: { string })
			app:setCopies(others)
		end
	)
end)

button.Click:Connect(function()
	widget.Enabled = not widget.Enabled
	if widget.Enabled then
		app.ui:focusPaste()
	end
end)
local function syncButton()
	button:SetActive(widget.Enabled)
end
widget:GetPropertyChangedSignal("Enabled"):Connect(syncButton)
syncButton()

local function lookForUpdates()
	if widget.Enabled then
		task.spawn(function()
			app:checkForUpdates()
		end)
	end
end
widget:GetPropertyChangedSignal("Enabled"):Connect(lookForUpdates)
lookForUpdates()

local pasteAction = plugin:CreatePluginAction(
	WIDGET_ID .. "_Paste",
	"Paste a design (" .. Brand.pluginName .. ")",
	"Open " .. Brand.pluginName .. ", ready to paste the code copied in " .. Brand.productName,
	ICON,
	true
)
pasteAction.Triggered:Connect(function()
	widget.Enabled = true
	app.ui:focusPaste()
end)
local fileAction = plugin:CreatePluginAction(
	WIDGET_ID .. "_ImportFile",
	"Import a design file (" .. Brand.pluginName .. ")",
	"Pick a ." .. Brand.bundleExtension .. " file from " .. Brand.productName .. " and import it",
	ICON,
	true
)
fileAction.Triggered:Connect(function()
	widget.Enabled = true
	app:importFile()
end)

plugin.Unloading:Connect(function()
	stopCopies()
	app:destroy()
end)
]]></ProtectedString>
		</Properties>
		<Item class="ModuleScript" referent="RBX0893A3B4941E80F3D6CC1268FC10CFB3">
			<Properties>
				<string name="Name">App</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local Brand = require(script.Parent.Brand)
local Builder = require(script.Parent.Builder)
local Bundle = require(script.Parent.Bundle)
local Errors = require(script.Parent.Errors)
local Export = require(script.Parent.Export)
local History = require(script.Parent.History)
local Images = require(script.Parent.Images)
local Paste = require(script.Parent.Paste)
local Settings = require(script.Parent.Settings)
local Ui = require(script.Parent.Ui)
local Updates = require(script.Parent.Updates)
local Uploader = require(script.Parent.Uploader)

local App = {}
App.__index = App

export type Deps = {
	host: Instance,
	settings: Settings.Settings,
	uploader: Images.Uploader,
	recorder: History.Recorder,
	pickFile: () -> (buffer?, string?),
	getSelection: () -> { Instance },
	setSelection: ({ Instance }) -> (),
	saveSelection: (fileName: string) -> boolean,
	service: (name: string) -> Instance,
	isRunning: () -> boolean,
	userId: () -> number,
	userName: (userId: number) -> string?,
	clock: () -> number,
	wait: (seconds: number) -> (),
	log: (text: string) -> (),
	random: Random,
	fetch: ((url: string) -> string)?,
	now: (() -> number)?,
}

type Pending = {
	bundle: Bundle.Bundle,
	plan: Builder.Plan,
	selection: { Instance },
	fallback: Instance,
	needs: Images.Needs,
	fileName: string,
	resolution: Images.Resolution?,
}

export type App = typeof(setmetatable(
	{} :: {
		deps: Deps,
		ui: Ui.Ui,
		busy: boolean,
		phase: string,
		pending: Pending?,
		collector: Paste.Collector,
		cancelRequested: boolean,
		userName: string?,
		newestPlugin: string?,
		newestRelease: number?,
		newestUrl: string?,
		otherCopies: { string },
		hidden: { [string]: boolean },
		updateOpen: boolean,
		checking: boolean,
		destroyed: boolean,
	},
	App
))

local NOTES_SHOWN = 3

local function plural(count: number, one: string, many: string?): string
	return string.format("%d %s", count, if count == 1 then one else many or one .. "s")
end

local function changes(added: number, updated: number, unchanged: number, removed: number, past: boolean): string
	local parts = {}
	if added > 0 then
		table.insert(parts, string.format(if past then "added %d" else "%d new", added))
	end
	if updated > 0 then
		table.insert(parts, string.format(if past then "updated %d" else "%d changed", updated))
	end
	if removed > 0 then
		table.insert(parts, string.format(if past then "removed %d" else "%d to remove", removed))
	end
	if #parts == 0 then
		return if past then "Nothing needed to change." else "Nothing changed since the last import."
	end
	local line = table.concat(parts, ", ")
	line = string.upper(string.sub(line, 1, 1)) .. string.sub(line, 2)
	if unchanged > 0 then
		line ..= string.format(" (%d unchanged)", unchanged)
	end
	return line .. "."
end

local function placeName(instance: Instance?): string
	if instance == nil then
		return "nowhere"
	end
	if instance.Parent == game then
		return instance.Name
	end
	return instance:GetFullName()
end

local function ago(at: number): string
	local seconds = os.time() - at
	if seconds < 60 then
		return "just now"
	elseif seconds < 3600 then
		return plural(seconds // 60, "minute") .. " ago"
	elseif seconds < 86400 then
		return plural(seconds // 3600, "hour") .. " ago"
	end
	return os.date("%d %b %Y", at)
end

local function firstNotes(notes: { string }, extra: { string }?): { string }
	local out = {}
	for index, note in notes do
		if index > NOTES_SHOWN then
			table.insert(out, string.format("…and %d more (all listed in the Output window).", #notes - NOTES_SHOWN))
			break
		end
		table.insert(out, note)
	end
	if extra then
		for _, note in extra do
			table.insert(out, note)
		end
	end
	return out
end

function App.new(deps: Deps): App
	local self: App
	local handlers: Ui.Handlers = {
		paste = function(text: string)
			self:paste(text)
		end,
		importFile = function()
			self:importFile()
		end,
		export = function()
			self:export()
		end,
		confirm = function()
			self:confirm()
		end,
		cancel = function()
			self:cancel()
		end,
		placeholders = function()
			self:continueWithoutUploads()
		end,
		dismiss = function()
			self:home()
		end,
		setTarget = function(value: string)
			self.deps.settings:setTarget(value)
			self:refreshSettings()
		end,
		setCreator = function(kind: string, groupId: number?)
			self.deps.settings:setCreator(kind, groupId)
			self:refreshSettings()
			local pending = self.pending
			if self.phase == "blocked" and not self.busy and pending and pending.resolution == nil and self:creator() then
				self:showSummary()
			end
		end,
		forgetUploads = function()
			self.deps.settings:forgetUploads()
			self:refreshSettings()
		end,
		hideNotice = function(id: string)
			self.hidden[id] = true
			self:refreshNotices()
		end,
		noticeAction = function(_id: string, action: string)
			self:updateAction(action)
		end,
		setUpdateCheck = function(on: boolean)
			self:setUpdateCheck(on)
		end,
	}
	deps.settings:migrate()
	self = setmetatable({
		deps = deps,
		ui = Ui.new(deps.host, handlers, {
			pluginName = Brand.pluginName,
			toolName = Brand.productName,
			guideUrl = Brand.guideUrl,
			version = Brand.pluginVersion,
			copyright = Brand.copyright,
		}),
		busy = false,
		phase = "idle",
		pending = nil :: Pending?,
		collector = Paste.collector(),
		cancelRequested = false,
		userName = nil :: string?,
		newestPlugin = nil :: string?,
		newestRelease = nil :: number?,
		newestUrl = nil :: string?,
		otherCopies = {} :: { string },
		hidden = {} :: { [string]: boolean },
		updateOpen = false,
		checking = false,
		destroyed = false,
	}, App)
	self:refreshSettings()
	self:home()
	self:refreshNotices()
	return self
end

function App.log(self: App, text: string)
	self.deps.log("[" .. Brand.pluginName .. "] " .. text)
end

function App.targetLabel(self: App, value: string): string
	if value == "file" then
		return "Where the file says (usually StarterGui)"
	end
	return value
end

function App.refreshSettings(self: App)
	local settings = self.deps.settings
	local choices = {}
	for _, value in Settings.TARGETS do
		table.insert(choices, { value = value, label = self:targetLabel(value) })
	end
	local creator = settings:creator()
	self.ui:setSettings({
		target = settings:target(),
		targetChoices = choices,
		creatorKind = creator.kind,
		groupId = creator.groupId,
		userName = self.userName,
		uploaded = settings:uploadedCount(),
		updateCheck = not settings:updates().off,
	})
end

function App.updateText(self: App, release: Updates.Release?): string
	return string.format(
		"%s %s is out (you have %s). Download it from %s and replace %s in your Plugins folder — your settings stay.",
		Brand.pluginName,
		if release then release.version else self.newestPlugin or "",
		Brand.pluginVersion,
		if release then release.url else self.newestUrl or Brand.guideUrl,
		Brand.pluginFileName
	)
end

function App.updateSteps(): { string }
	return {
		"Copy the guide's address into your browser and download " .. Brand.pluginFileName .. ":",
		"Put it in your Plugins folder (Plugins tab → Plugins Folder) in place of the old " .. Brand.pluginFileName .. ".",
		"Restart Studio. Your settings and uploads stay.",
	}
end

function App.newerRelease(self: App): Updates.Release?
	local pasted: Updates.Release? = nil
	if self.newestPlugin and self.newestRelease then
		pasted = { version = self.newestPlugin, release = self.newestRelease, url = self.newestUrl or Brand.guideUrl }
	end
	return Updates.shown(self.deps.settings:updates(), pasted, Brand.pluginRelease)
end

function App.updateNotice(self: App, release: Updates.Release): Ui.Notice
	local open = self.updateOpen
	local actions: { Ui.Action } = {
		{ id = "UpdateHow", label = if open then "Hide steps" else "How to update" },
		{ id = "UpdateLater", label = "Later" },
	}
	if self.deps.fetch and not self.deps.settings:updates().off then
		table.insert(actions, { id = "UpdateOff", label = "Don't check" })
	end
	return {
		id = "update " .. release.version,
		title = string.format("%s %s is out", Brand.pluginName, release.version),
		text = release.notes or string.format("You have %s. Updating keeps your settings.", Brand.pluginVersion),
		steps = if open then App.updateSteps() else nil,
		link = if open then release.url else nil,
		actions = actions,
		tone = "info",
	}
end

function App.updateAction(self: App, action: string)
	if action == "UpdateHow" then
		self.updateOpen = not self.updateOpen
		self:refreshNotices()
		return
	end
	if action ~= "UpdateLater" and action ~= "UpdateOff" then
		return
	end
	local release = self:newerRelease()
	local state = table.clone(self.deps.settings:updates())
	if release then
		state.later = math.max(state.later or 0, release.release)
	end
	if action == "UpdateOff" then
		state.off = true
	end
	self.deps.settings:setUpdates(state)
	self.updateOpen = false
	self:refreshSettings()
	self:refreshNotices()
end

function App.setUpdateCheck(self: App, on: boolean)
	local state = table.clone(self.deps.settings:updates())
	if state.off == not on then
		return
	end
	state.off = not on
	self.deps.settings:setUpdates(state)
	self:refreshSettings()
	self:refreshNotices()
	if on then
		self:checkForUpdates()
	end
end

function App.checkForUpdates(self: App)
	local fetch = self.deps.fetch
	if fetch == nil or self.checking or self.destroyed then
		return
	end
	self.checking = true
	local now = if self.deps.now then self.deps.now() else os.time()
	local ok, found = pcall(Updates.check, self.deps.settings, fetch, now)
	self.checking = false
	if self.destroyed then
		return
	end
	local shown = self:newerRelease()
	if ok and found and shown and shown.release == found.release then
		self:log(self:updateText(shown))
	end
	self:refreshNotices()
end

local function copiesText(others: { string }): (string, string)
	local count = #others + 1
	local title = Brand.pluginName .. " is installed " .. (if count == 2 then "twice" else count .. " times")
	local same = true
	local names = {}
	for _, version in others do
		same = same and version == Brand.pluginVersion
		table.insert(names, if version == "" then "an older version" else "v" .. version)
	end
	local files = if count == 2 then "file" else "files"
	local folder = "your Plugins folder (Plugins tab → Plugins Folder), then restart Studio. Your settings stay."
	if same then
		return title, string.format("All copies are v%s. Keep one: remove the other %s from %s", Brand.pluginVersion, files, folder)
	end
	return title,
		string.format(
			"This one is v%s; the other %s %s. Keep the newest: remove the other %s from %s",
			Brand.pluginVersion,
			if count == 2 then "is" else "ones are",
			table.concat(names, " and "),
			files,
			folder
		)
end

function App.refreshNotices(self: App)
	if self.destroyed then
		return
	end
	local notices: { Ui.Notice } = {}
	if #self.otherCopies > 0 and not self.hidden.copies then
		local title, text = copiesText(self.otherCopies)
		table.insert(notices, { id = "copies", title = title, text = text, tone = "warn" })
	end
	local release = self:newerRelease()
	if release and not self.hidden["update " .. release.version] then
		table.insert(notices, self:updateNotice(release))
	end
	self.ui:setNotices(notices)
end

function App.noteNewestPlugin(self: App, bundle: Bundle.Bundle)
	local version, release = bundle.pluginVersion, bundle.pluginRelease
	if version == nil or release == nil or release <= Brand.pluginRelease then
		return
	end
	local known = self.newestRelease
	if known and release <= known then
		return
	end
	self.newestPlugin = version
	self.newestRelease = release
	self.newestUrl = bundle.pluginUrl or Brand.guideUrl
	self:log(self:updateText(nil))
	self:refreshNotices()
end

function App.setCopies(self: App, others: { string })
	self.otherCopies = others
	if #others > 0 then
		local title, text = copiesText(others)
		self:log(title .. ". " .. text)
	end
	self:refreshNotices()
end

function App.loadUserName(self: App)
	local id = self.deps.userId()
	if id <= 0 then
		return
	end
	local ok, name = pcall(self.deps.userName, id)
	if ok and type(name) == "string" and not self.destroyed then
		self.userName = name
		self:refreshSettings()
	end
end

function App.home(self: App)
	if self.busy then
		return
	end
	self.phase = "idle"
	self.pending = nil
	local settings = self.deps.settings
	local target = settings:target()
	local goes = if target == "file" then "StarterGui (or where the file says)" else target
	local notes = { "New designs go to " .. goes .. ". To update an earlier import, select it before importing." }
	local last = settings:lastImport()
	if last then
		local lines = { changes(last.added, last.updated, 0, last.removed, true) }
		if last.uploaded > 0 then
			table.insert(lines, "Uploaded " .. plural(last.uploaded, "image") .. ".")
		end
		if last.empty > 0 then
			table.insert(lines, plural(last.empty, "image") .. " left empty. Import again to upload them.")
		end
		self.ui:show({
			view = "home",
			title = "Last import",
			text = '"' .. last.project .. '", ' .. ago(last.at) .. ".",
			lines = lines,
			notes = notes,
		})
	else
		self.ui:show({ view = "home", title = "", notes = notes })
	end
end

function App.showError(self: App, title: string, err: any, extra: string?)
	self:log(title .. ": " .. Errors.detail(err))
	self.busy = false
	self.phase = "idle"
	self.pending = nil
	local update = Errors.kind(err) == "update"
	self.ui:show({
		view = "error",
		title = if update then "Update " .. Brand.pluginName else title,
		text = Errors.message(err) .. (if extra then " " .. extra else ""),
		link = if update then Brand.guideUrl else nil,
		tone = if update then "warn" else "error",
		actions = { { id = "dismiss", label = "OK" } },
	})
end

function App.working(self: App, title: string, text: string?, progress: number?, cancellable: boolean?)
	self.ui:show({
		view = "working",
		title = title,
		text = text,
		progress = progress,
		busy = true,
		actions = if cancellable then { { id = "cancel", label = "Cancel", primary = false } } else nil,
	})
end

function App.fallback(self: App, bundle: Bundle.Bundle): Instance
	local target = self.deps.settings:target()
	return self.deps.service(if target == "file" then bundle.target else target)
end

function App.creator(self: App): Images.Creator?
	local creator = self.deps.settings:creator()
	if creator.kind == "group" then
		if creator.groupId then
			return { kind = "group", id = creator.groupId }
		end
		return nil
	end
	return { kind = "user", id = self.deps.userId() }
end

function App.paste(self: App, text: string)
	if self.busy then
		return
	end
	local found, parts = xpcall(Paste.find, Errors.handler, text)
	if not found then
		self:showError("Couldn't read the code", parts)
		return
	end
	local state = Paste.add(self.collector, parts)
	if not state.complete then
		self.phase = "pasting"
		self.pending = nil
		local notes = { "The parts can be pasted in any order." }
		if state.restarted then
			table.insert(notes, 1, "That part belongs to another export, so the parts pasted before it were set aside.")
		end
		local segments = {}
		for number = 1, state.need do
			segments[number] = table.find(state.missing, number) == nil
		end
		self.ui:show({
			view = "pasting",
			title = "Paste the next part",
			text = Paste.describe(state) .. " Copy the next part in " .. Brand.productName .. ", then paste it in the box above.",
			progress = state.have / state.need,
			segments = segments,
			notes = notes,
			tone = "info",
			actions = { { id = "cancel", label = "Start over", primary = false } },
		})
		return
	end
	local collector = self.collector
	self.collector = Paste.collector()
	self:startImport("the pasted code", function(): (buffer?, string?)
		return Paste.assemble(collector), "the pasted code"
	end)
end

function App.importFile(self: App)
	self:startImport("Choose a design file", function(): (buffer?, string?)
		return self.deps.pickFile()
	end)
end

function App.startImport(self: App, title: string, getBytes: () -> (buffer?, string?))
	if self.busy then
		return
	end
	if self.deps.isRunning() then
		self:showError("Can't import now", Errors.new("Stop the playtest first: changes made while testing are lost when it stops."))
		return
	end
	self.busy = true
	self.phase = "picking"
	self.pending = nil
	if title == "Choose a design file" then
		self:working(title, "Pick the ." .. Brand.bundleExtension .. " file you exported from " .. Brand.productName .. ".")
	else
		self:working("Reading " .. title, nil)
		self.deps.wait(0)
	end
	local read = function()
		local bytes, fileName = getBytes()
		if bytes == nil then
			return
		end
		local name = fileName or "the file"
		self:working("Reading " .. name, nil)
		self.deps.wait(0)
		local bundle = Bundle.parse(bytes)
		self:noteNewestPlugin(bundle)
		for _, warning in bundle.warnings do
			self:log(warning)
		end
		local selection = self.deps.getSelection()
		local fallback = self:fallback(bundle)
		local plan = Builder.plan(bundle, { selection = selection, fallback = fallback })
		local needs = Images.needs(bundle, self.deps.settings:imageCache())
		self.pending = {
			bundle = bundle,
			plan = plan,
			selection = selection,
			fallback = fallback,
			needs = needs,
			fileName = name,
			resolution = nil,
		}
	end
	local ok, err = xpcall(read :: any, Errors.handler)
	self.busy = false
	if not ok then
		self:showError(if title == "Choose a design file" then "Couldn't read the file" else "Couldn't read the code", err)
		return
	end
	if self.pending == nil then
		self:home()
		return
	end
	self:showSummary()
end

function App.showSummary(self: App)
	local pending = self.pending
	if pending == nil then
		return
	end
	self.phase = "summary"
	local bundle, plan, needs = pending.bundle, pending.plan, pending.needs
	local lines = {}
	local rootKeys: { [string]: boolean } = {}
	for _, target in plan.targets do
		rootKeys[target.root.key] = true
	end
	for _, target in plan.targets do
		local owner = string.match(target.root.key, "^(.-):")
		if owner and rootKeys[owner] then
			continue
		end
		local existing = target.existing
		local holder = plan.holder
		if existing then
			table.insert(lines, 'Updates "' .. existing.Name .. '" in ' .. placeName(existing.Parent))
		else
			if target.inHolder and holder then
				table.insert(lines, 'Adds "' .. target.root.name .. '" to a new ScreenGui "' .. holder.name .. '" in ' .. placeName(target.parent))
			else
				table.insert(lines, 'Adds "' .. target.root.name .. '" to ' .. placeName(target.parent))
			end
			local ignored = target.ignored
			if ignored then
				table.insert(
					lines,
					string.format("(The selected %s %q can't hold a %s.)", ignored.ClassName, ignored.Name, target.root.className)
				)
			end
		end
	end
	table.insert(lines, changes(plan.added, plan.updated, plan.unchanged, plan.removed, false))
	local known = 0
	for _ in needs.known do
		known += 1
	end
	local extra = {}
	if #needs.upload > 0 then
		local line = plural(#needs.upload, "image") .. " to upload"
		if known > 0 then
			line ..= ", " .. known .. " already on Roblox"
		end
		table.insert(lines, line .. ".")
		local creator = self.deps.settings:creator()
		table.insert(
			extra,
			if creator.kind == "group"
				then "Images are uploaded to group " .. (if creator.groupId then tostring(creator.groupId) else "(no id yet: set it in Settings)") .. "."
				else "Images are uploaded to your account" .. (if self.userName then " (" .. self.userName .. ")" else "") .. "."
		)
	elseif known > 0 then
		table.insert(lines, plural(known, "image") .. ", all already on Roblox.")
	end
	if #needs.missing > 0 then
		table.insert(lines, plural(#needs.missing, "image") .. " missing from the file (left empty).")
	end
	self.ui:show({
		view = "summary",
		title = "Ready to import",
		text = 'Design: "' .. bundle.projectName .. '"',
		lines = lines,
		warnings = firstNotes(bundle.warnings),
		notes = extra,
		tone = "info",
		actions = { { id = "confirm", label = "Import", primary = true }, { id = "cancel", label = "Cancel", primary = false } },
	})
end

function App.confirm(self: App)
	local pending = self.pending
	if self.busy or pending == nil or self.phase ~= "summary" then
		return
	end
	if self.deps.isRunning() then
		self:showError("Can't import now", Errors.new("Stop the playtest first: changes made while testing are lost when it stops."))
		return
	end
	local creator = self:creator()
	if #pending.needs.upload > 0 and creator == nil then
		self.phase = "blocked"
		self.ui:show({
			view = "blocked",
			title = "Which group gets the images?",
			text = 'Images go to a group, but its id is missing. Open Settings and type the group id, or pick "Me".',
			tone = "warn",
			actions = {
				{ id = "settings", label = "Open Settings", primary = true },
				{ id = "placeholders", label = "Import with empty images", primary = false },
				{ id = "cancel", label = "Cancel", primary = false },
			},
		})
		return
	end
	self.busy = true
	self.phase = "uploading"
	self.cancelRequested = false
	local total = #pending.needs.upload
	if total > 0 then
		self:working("Uploading images", "Getting ready…", 0, true)
	end
	local uploadNote = "If Studio asks whether " .. Brand.pluginName .. " may upload, choose Allow: it uploads only this design's images, so they show in your game."
	local ok, resolution = xpcall(Images.resolve, Errors.handler, pending.bundle, {
		uploader = self.deps.uploader,
		cache = self.deps.settings:imageCache(),
		creator = creator or { kind = "user", id = 0 },
		description = string.format('Uploaded by the %s plugin for the design "%s".', Brand.pluginName, pending.bundle.projectName),
		allowUpload = true,
		isCancelled = function()
			return self.cancelRequested or self.destroyed
		end,
		progress = function(done: number, count: number, text: string)
			if not self.cancelRequested then
				self.ui:show({
					view = "working",
					title = "Uploading images",
					text = text,
					progress = if count > 0 then done / count else nil,
					notes = { uploadNote },
					busy = true,
					actions = { { id = "cancel", label = "Cancel", primary = false } },
				})
			end
		end,
		clock = self.deps.clock,
		wait = self.deps.wait,
	})
	self.busy = false
	if not ok then
		self:showError("Couldn't upload the images", resolution)
		return
	end
	pending.resolution = resolution
	for _, message in resolution.errors do
		self:log(message)
	end
	if resolution.cancelled then
		self.phase = "idle"
		self.pending = nil
		self.ui:show({
			view = "done",
			title = "Import cancelled",
			text = "Nothing was changed in your place.",
			lines = if resolution.uploaded > 0
				then { plural(resolution.uploaded, "image") .. " uploaded so far; the next import won't upload them again." }
				else nil,
			tone = "info",
			actions = { { id = "dismiss", label = "OK" } },
		})
		self:refreshSettings()
		return
	end
	local blocked = resolution.blocked
	if blocked then
		self:log("Uploads are not allowed: " .. blocked.message)
		self.phase = "blocked"
		self.ui:show({
			view = "blocked",
			title = "Images can't be uploaded",
			text = blocked.message,
			steps = Uploader.HELP,
			notes = if resolution.uploaded > 0
				then { plural(resolution.uploaded, "image") .. " uploaded before this are remembered." }
				else { "You can import now with empty images, and import again once uploads work." },
			tone = "warn",
			actions = {
				{ id = "placeholders", label = "Import with empty images", primary = true },
				{ id = "cancel", label = "Cancel", primary = false },
			},
		})
		self:refreshSettings()
		return
	end
	self:build()
end

function App.continueWithoutUploads(self: App)
	local pending = self.pending
	if self.busy or pending == nil or self.phase ~= "blocked" then
		return
	end
	if pending.resolution == nil then
		local ok, resolution = xpcall(Images.resolve, Errors.handler, pending.bundle, {
			uploader = self.deps.uploader,
			cache = self.deps.settings:imageCache(),
			creator = { kind = "user", id = 0 },
			description = "",
			allowUpload = false,
			isCancelled = function()
				return false
			end,
			progress = function() end,
			clock = self.deps.clock,
			wait = self.deps.wait,
		})
		if not ok then
			self:showError("Couldn't import", resolution)
			return
		end
		pending.resolution = resolution
	end
	self:build()
end

function App.build(self: App)
	local pending = self.pending
	local resolution = pending and pending.resolution
	if pending == nil or resolution == nil then
		return
	end
	if self.deps.isRunning() then
		self:showError("Can't import now", Errors.new("Stop the playtest first: changes made while testing are lost when it stops."))
		return
	end
	self.busy = true
	self.phase = "building"
	self:working("Building", "Putting the design in place…")
	self.deps.wait(0)
	local bundle = pending.bundle
	local selection = {}
	for _, instance in pending.selection do
		if instance:IsDescendantOf(game) then
			table.insert(selection, instance)
		end
	end
	local result: Builder.Result? = nil
	local ok, err = xpcall(function()
		local plan = Builder.plan(bundle, { selection = selection, fallback = pending.fallback })
		local recorded, failure = History.run(self.deps.recorder, "NKD_Import", Brand.pluginName .. ": import " .. bundle.projectName, function()
			local built = Builder.apply(bundle, plan, resolution.contents)
			result = built
			if #built.scrolls > 0 then
				self.deps.wait(0)
				Builder.settleScrolls(built)
			end
		end)
		if not recorded then
			error(failure, 0)
		end
	end, Errors.handler)
	self.busy = false
	self.pending = nil
	local built = result
	if not ok or built == nil then
		self:showError("Couldn't import", err, "Nothing in your place was changed.")
		return
	end
	self.deps.setSelection(built.roots)
	for _, problem in built.problems do
		self:log(problem)
	end

	local empty = resolution.missing
	self.deps.settings:setLastImport({
		project = bundle.projectName,
		at = os.time(),
		added = built.added,
		updated = built.updated,
		removed = built.removed,
		uploaded = resolution.uploaded,
		empty = empty,
	})
	local lines = { changes(built.added, built.updated, built.unchanged, built.removed, true) }
	if resolution.uploaded > 0 then
		table.insert(lines, "Uploaded " .. plural(resolution.uploaded, "image") .. ". New images can take a minute to show while Roblox checks them.")
	end
	if empty > 0 then
		table.insert(lines, plural(empty, "image") .. " left empty. Import again to upload " .. (if empty == 1 then "it" else "them") .. ".")
	end
	if built.moved > 0 then
		table.insert(lines, "Moved " .. plural(built.moved, "of your objects", "of your objects") .. " out of layers that were removed or replaced, so nothing of yours was lost.")
	end
	local notes = {}
	for _, message in resolution.errors do
		table.insert(notes, message)
	end
	for _, problem in built.problems do
		table.insert(notes, problem)
	end
	self.phase = "idle"
	self.ui:show({
		view = "done",
		title = "Imported",
		lines = lines,
		warnings = firstNotes(notes),
		undo = true,
		tone = if empty > 0 or #notes > 0 then "warn" else "ok",
		actions = { { id = "dismiss", label = "Done" } },
	})
	self:log(string.format("Imported \"%s\": %s", bundle.projectName, lines[1]))
	self:refreshSettings()
end

function App.cancel(self: App)
	if self.phase == "pasting" then
		self.collector = Paste.collector()
	end
	if self.phase == "uploading" then
		self.cancelRequested = true
		self:working("Uploading images", "Stopping after the current image…", nil, false)
		return
	end
	if self.busy then
		return
	end
	self:home()
end

function App.export(self: App)
	if self.busy then
		return
	end
	local selection = self.deps.getSelection()
	local nothing = false
	local ok, err = xpcall((function()
		local hasUi = false
		for _, instance in selection do
			if Export.isUi(instance) or instance:FindFirstChildWhichIsA("GuiBase2d", true) then
				hasUi = true
				break
			end
		end
		if not hasUi then
			nothing = true
			return
		end
		self.busy = true
		self.phase = "exporting"
		self:working(
			"Save for " .. Brand.productName,
			'In the save window, pick "Roblox XML Model Files (*.rbxmx)" as the file type, then save.'
		)
		local uploads = self.deps.settings:imageCache()
		local stamp = Export.stamp()
		local prepared: Export.Prepared? = Export.prepare(selection, self.deps.random, true, uploads, stamp)
		if prepared and prepared.assigned + prepared.cleared + prepared.stamped > 0 then
			local recorded, failure = History.run(self.deps.recorder, "NKD_Export", Brand.pluginName .. ": prepare UI for " .. Brand.productName, function()
				prepared = Export.prepare(selection, self.deps.random, false, uploads, stamp)
			end)
			if not recorded then
				error(failure, 0)
			end
		end
		if prepared == nil then
			error("no export", 0)
		end
		local ready = prepared :: Export.Prepared
		self.deps.setSelection(ready.roots)
		local saved = self.deps.saveSelection(Export.fileName(ready.roots))
		self.busy = false
		self.phase = "idle"
		if saved then
			self.ui:show({
				view = "done",
				title = "Saved for " .. Brand.productName,
				text = "In " .. Brand.productName .. ", click Import and pick the file you just saved.",
				lines = { plural(ready.count, "UI object") .. " in the file." },
				notes = if ready.assigned > 0
					then { "Gave " .. plural(ready.assigned, "object") .. " an id, so later imports update the same objects (Ctrl+Z removes the ids)." }
					else nil,
				tone = "ok",
				actions = { { id = "dismiss", label = "Done" } },
			})
		else
			self.ui:show({
				view = "done",
				title = "Not saved",
				text = "The save window was closed. Click Export to designer to try again.",
				tone = "info",
				actions = { { id = "dismiss", label = "OK" } },
			})
		end
	end) :: any, Errors.handler)
	if not ok then
		self.busy = false
		self:showError("Couldn't export", err)
	elseif nothing then
		self.ui:show({
			view = "error",
			title = "Select some UI first",
			text = "Select the UI to send to " .. Brand.productName .. " first: a ScreenGui, a Frame, or anything with UI inside. Then click Export to designer.",
			tone = "info",
			actions = { { id = "dismiss", label = "OK" } },
		})
	end
end

function App.destroy(self: App)
	self.destroyed = true
	self.cancelRequested = true
	self.ui:destroy()
end

return App
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBXD33008A6F4CBFEE036FDF5D229FE4295">
			<Properties>
				<string name="Name">Attributes</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local Brand = require(script.Parent.Brand)

local Attributes = {}

local NAMES: { [string]: string } = Brand.attributes :: any
local OLD_NAMES: { [string]: string } = Brand.legacyAttributes :: any

local RENAMED: { [string]: string } = {}
for key, name in NAMES do
	local old = OLD_NAMES[key]
	if old and old ~= name then
		RENAMED[old] = name
	end
end

function Attributes.get(instance: Instance, key: string): any
	local value = instance:GetAttribute(NAMES[key])
	if value == nil then
		value = instance:GetAttribute(OLD_NAMES[key])
	end
	return value
end

function Attributes.find(attributes: { [string]: any }, key: string): any
	local value = attributes[NAMES[key]]
	if value == nil then
		value = attributes[OLD_NAMES[key]]
	end
	return value
end

function Attributes.current(name: string): string
	return RENAMED[name] or name
end

function Attributes.isDesigner(name: string): boolean
	for _, prefix in Brand.attributePrefixes do
		if string.sub(name, 1, #prefix) == prefix then
			return true
		end
	end
	return false
end

function Attributes.rename(instance: Instance)
	for old, name in RENAMED do
		local value = instance:GetAttribute(old)
		if value ~= nil then
			if instance:GetAttribute(name) == nil then
				instance:SetAttribute(name, value)
			end
			instance:SetAttribute(old, nil)
		end
	end
end

return Attributes
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBXBF26D43F678C5BCAE751EB383C34D78E">
			<Properties>
				<string name="Name">Brand</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

return table.freeze({
	pluginName = "Nighty",
	pluginFileName = "Nighty.rbxmx",
	productName = "Nighty Design",
	appVersion = "1.0.0",
	pluginVersion = "1.0.4",
	pluginRelease = 12,
	toolbarIcon = "rbxassetid://106890984663098",
	copyright = "© 2026 NightyCode",
	siteOrigin = "https://nightkilleryt.com",
	toolUrl = "https://nightkilleryt.com/tools/ui-designer",
	guideUrl = "https://nightkilleryt.com/tools/ui-designer/guide",
	versionUrl = "https://nightkilleryt.com/assets/designer/nighty-version.json",
	pluginAttribute = "NCD_Plugin",
	sentAttribute = "NCD_Sent",
	bundleExtension = "ncui",
	legacyBundleExtensions = table.freeze({ "nkui" }),
	pastePrefix = "NCUI1",
	legacyPastePrefixes = table.freeze({ "NKUI1" }),
	pastePartChars = 190000,
	attributes = table.freeze({
		id = "NCD_Id",
		project = "NCD_Project",
		hash = "NCD_Hash",
		image = "NCD_Image",
		holder = "NCD_Holder",
		shadow = "NCD_Shadow",
	}),
	legacyAttributes = table.freeze({
		id = "NKD_Id",
		project = "NKD_Project",
		hash = "NKD_Hash",
		image = "NKD_Image",
		holder = "NKD_Holder",
		shadow = "NKD_Shadow",
	}),
	attributePrefixes = table.freeze({ "NCD_", "NKD_" }),
})
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBXFFAE9334EBDE9F0E97AEAEFB2A778A62">
			<Properties>
				<string name="Name">Builder</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local Attributes = require(script.Parent.Attributes)
local Brand = require(script.Parent.Brand)
local Bundle = require(script.Parent.Bundle)
local Rij = require(script.Parent.Rij)

type Node = Bundle.Node

local ATTR_ID = Brand.attributes.id
local ATTR_PROJECT = Brand.attributes.project
local ATTR_HASH = Brand.attributes.hash
local ATTR_IMAGE = Brand.attributes.image
local ATTR_HOLDER = Brand.attributes.holder
local ATTR_SENT = Brand.sentAttribute

local SERVICES = { StarterGui = true, ReplicatedStorage = true, ServerStorage = true, Workspace = true }

local Builder = {}

export type Options = {
	selection: { Instance },
	fallback: Instance,
}

export type Target = {
	root: Node,
	existing: Instance?,
	parent: Instance,
	inHolder: boolean,
	ignored: Instance?,
}

export type HolderPlan = {
	name: string,
	properties: { [string]: any },
}

export type Plan = {
	targets: { Target },
	holder: HolderPlan?,
	added: number,
	updated: number,
	unchanged: number,
	removed: number,
}

export type Result = {
	roots: { Instance },
	added: number,
	updated: number,
	unchanged: number,
	removed: number,
	moved: number,
	placeholders: number,
	problems: { string },
	scrolls: { { instance: Instance, position: Vector2 } },
}

type Index = {
	byKey: { [string]: Instance },
	stale: { Instance },
	design: { [Instance]: boolean },
}

local function keysOf(bundle: Bundle.Bundle): { [string]: boolean }
	local keys: { [string]: boolean } = {}
	Bundle.walk(bundle.roots, function(node)
		keys[node.key] = true
	end)
	return keys
end

local function scan(roots: { Instance }, keys: { [string]: boolean }, projectId: string): Index
	local index: Index = { byKey = {}, stale = {}, design = {} }
	local visited: { [Instance]: boolean } = {}
	local function visit(instance: Instance, isRoot: boolean)
		if visited[instance] then
			return
		end
		visited[instance] = true
		if not isRoot then
			local project = Attributes.get(instance, "project")
			if project ~= nil and project ~= projectId then
				return
			end
		end
		local id = Attributes.get(instance, "id")
		if type(id) == "string" and id ~= "" then
			if keys[id] then
				if not index.byKey[id] then
					index.byKey[id] = instance
					index.design[instance] = true
				end
			else
				table.insert(index.stale, instance)
				index.design[instance] = true
			end
		end
		for _, child in instance:GetChildren() do
			visit(child, false)
		end
	end
	for _, root in roots do
		visit(root, true)
	end
	return index
end

local function importRootOf(instance: Instance, projectId: string): Instance?
	local current: Instance? = instance
	while current and current ~= game do
		if Attributes.get(current, "project") == projectId then
			return current
		end
		current = current.Parent
	end
	return nil
end

local function findRoot(container: Instance, key: string, projectId: string): Instance?
	local function matches(instance: Instance): boolean
		return Attributes.get(instance, "id") == key and importRootOf(instance, projectId) ~= nil
	end
	if matches(container) then
		return container
	end
	for _, instance in container:GetDescendants() do
		if matches(instance) then
			return instance
		end
	end
	return nil
end

local function selectedRootOf(instance: Instance, projectId: string, rootKeys: { [string]: boolean }): Instance?
	if importRootOf(instance, projectId) == nil then
		return nil
	end
	local current: Instance? = instance
	while current and current ~= game do
		local id = Attributes.get(current, "id")
		if type(id) == "string" and rootKeys[id] then
			return current
		end
		current = current.Parent
	end
	return nil
end

local function isService(instance: Instance): boolean
	return instance.Parent == game and SERVICES[instance.ClassName] == true
end

local function holdsGui(instance: Instance): boolean
	return instance:IsA("GuiObject") or instance:IsA("LayerCollector")
end

local function holdsScreens(instance: Instance): boolean
	return instance:IsA("Folder") or instance:IsA("Model") or instance:IsA("PlayerGui") or isService(instance)
end

local function kindOf(className: string): string
	if className == "ScreenGui" or className == "Folder" then
		return "screen"
	elseif className == "SurfaceGui" or className == "BillboardGui" then
		return "world"
	end
	return "gui"
end

local function fits(container: Instance, className: string): boolean
	local kind = kindOf(className)
	if kind == "screen" then
		return holdsScreens(container)
	elseif kind == "world" then
		return holdsScreens(container) or container:IsA("BasePart") or container:IsA("Attachment")
	end
	return holdsGui(container) or holdsScreens(container)
end

local function staleSiblings(root: Instance, key: string, keys: { [string]: boolean }, projectId: string, into: { Instance })
	local parent = root.Parent
	if parent == nil then
		return
	end
	local prefix = key .. ":"
	for _, sibling in parent:GetChildren() do
		local id = Attributes.get(sibling, "id")
		if
			sibling ~= root
			and type(id) == "string"
			and string.sub(id, 1, #prefix) == prefix
			and not keys[id]
			and Attributes.get(sibling, "project") == projectId
			and not table.find(into, sibling)
		then
			table.insert(into, sibling)
		end
	end
end

function Builder.plan(bundle: Bundle.Bundle, options: Options): Plan
	local rootKeys: { [string]: boolean } = {}
	for _, root in bundle.roots do
		rootKeys[root.key] = true
	end

	local selectedRoots: { [string]: Instance } = {}
	local anchor: Instance? = nil
	for _, selected in options.selection do
		local found = selectedRootOf(selected, bundle.projectId, rootKeys)
		local id = found and Attributes.get(found, "id")
		if found and type(id) == "string" and not selectedRoots[id] then
			selectedRoots[id] = found
			anchor = anchor or found
		end
	end
	local container: Instance? = nil
	if anchor == nil and #options.selection == 1 then
		container = options.selection[1]
	end

	local targets: { Target } = {}
	for _, root in bundle.roots do
		local parent = options.fallback
		local ignored: Instance? = nil
		local anchorParent = anchor and anchor.Parent
		if anchorParent then
			parent = anchorParent
		elseif container and fits(container, root.className) then
			parent = container
		elseif container then
			ignored = container
		end
		local existing = selectedRoots[root.key] or findRoot(parent, root.key, bundle.projectId)
		local inHolder = existing == nil and kindOf(root.className) == "gui" and not holdsGui(parent)
		table.insert(targets, { root = root, existing = existing, parent = parent, inHolder = inHolder, ignored = ignored })
	end

	local looseHome: Instance? = nil
	for _, target in targets do
		local existing = target.existing
		if existing and existing.Parent and kindOf(target.root.className) == "gui" then
			looseHome = existing.Parent
			break
		end
	end
	local needsHolder = false
	for _, target in targets do
		if target.existing == nil and kindOf(target.root.className) == "gui" then
			if looseHome then
				target.parent = looseHome
				target.inHolder = false
			elseif target.inHolder then
				needsHolder = true
			end
		end
	end
	local holder: HolderPlan? = nil
	if needsHolder then
		local template = bundle.holder
		holder = {
			name = if template then template.name else bundle.projectName,
			properties = if template
				then template.properties
				else { ResetOnSpawn = false, ZIndexBehavior = Enum.ZIndexBehavior.Sibling },
		}
	end

	local existingRoots = {}
	for _, target in targets do
		if target.existing then
			table.insert(existingRoots, target.existing :: Instance)
		end
	end
	local keys = keysOf(bundle)
	local index = scan(existingRoots, keys, bundle.projectId)
	for _, target in targets do
		local existing = target.existing
		if existing then
			staleSiblings(existing, target.root.key, keys, bundle.projectId, index.stale)
		end
	end
	local plan: Plan = { targets = targets, holder = holder, added = 0, updated = 0, unchanged = 0, removed = #index.stale }
	Bundle.walk(bundle.roots, function(node)
		local instance: Instance? = index.byKey[node.key]
		if instance == nil then
			plan.added += 1
		elseif instance.ClassName == node.className and node.hash ~= nil and Attributes.get(instance, "hash") == node.hash then
			plan.unchanged += 1
		else
			plan.updated += 1
		end
	end)
	return plan
end

local defaultsCache: { [string]: { [string]: any } } = {}
local function defaultsOf(className: string): { [string]: any }
	local cached = defaultsCache[className]
	if cached then
		return cached
	end
	local values: { [string]: any } = {}
	local ok, sample = pcall(Instance.new, className)
	if ok and sample then
		for _, property in Rij.designProperties(className) do
			local readOk, value = pcall(function()
				return (sample :: any)[property]
			end)
			if readOk then
				values[property] = value
			end
		end
		sample:Destroy()
	end
	defaultsCache[className] = values
	return values
end

local function assign(instance: any, property: string, value: any)
	if instance[property] ~= value then
		instance[property] = value
	end
end

local function readSent(instance: Instance): { [string]: string }
	local sent: { [string]: string } = {}
	local raw = instance:GetAttribute(ATTR_SENT)
	if type(raw) == "string" then
		for line in string.gmatch(raw, "[^\n]+") do
			local name, value = string.match(line, "^([%w_]+)=(.*)$")
			if name and value then
				sent[name] = value
			end
		end
	end
	return sent
end

local function sentText(sent: { [string]: string }): string?
	local names = {}
	for name in sent do
		table.insert(names, name)
	end
	if #names == 0 then
		return nil
	end
	table.sort(names)
	local lines = {}
	for _, name in names do
		table.insert(lines, name .. "=" .. sent[name])
	end
	return table.concat(lines, "\n")
end

function Builder.apply(bundle: Bundle.Bundle, plan: Plan, images: { [string]: string }): Result
	local result: Result = {
		roots = {},
		added = 0,
		updated = 0,
		unchanged = 0,
		removed = 0,
		moved = 0,
		placeholders = 0,
		problems = {},
		scrolls = {},
	}
	local existingRoots = {}
	for _, target in plan.targets do
		local existing = target.existing
		if existing and existing.Parent then
			table.insert(existingRoots, existing)
		end
	end
	local keys = keysOf(bundle)
	local index = scan(existingRoots, keys, bundle.projectId)
	for _, target in plan.targets do
		local existing = target.existing
		if existing and existing.Parent then
			staleSiblings(existing, target.root.key, keys, bundle.projectId, index.stale)
		end
	end
	for _, instance in index.stale do
		index.design[instance] = true
	end
	local replaced: { { old: Instance, new: Instance } } = {}

	local function problem(node: Node, what: string, err: any)
		local reason = string.gsub(tostring(err), "^.-:%d+: ", "")
		table.insert(result.problems, node.path .. ": Studio didn't accept " .. what .. " (" .. reason .. ")")
	end

	local function set(instance: Instance, node: Node, property: string, value: any)
		local ok, err = pcall(assign :: any, instance, property, value)
		if not ok then
			problem(node, property, err)
		end
	end

	local function setAttribute(instance: Instance, node: Node, name: string, value: any)
		if instance:GetAttribute(name) == value then
			return
		end
		local ok, err = pcall(instance.SetAttribute, instance, name, value)
		if not ok then
			problem(node, "the attribute " .. name, err)
		end
	end

	local function applyLayer(instance: Instance, node: Node, fresh: boolean)
		if not fresh then
			local defaults = defaultsOf(node.className)
			for _, property in Rij.designProperties(node.className) do
				if node.properties[property] == nil and defaults[property] ~= nil then
					set(instance, node, property, defaults[property])
				end
			end
		end
		local pending = false
		local sentBefore = if fresh then {} else readSent(instance)
		local sent: { [string]: string } = {}
		for _, property in Rij.properties(node.className) do
			local value = node.properties[property]
			if value == nil then
				continue
			end
			local missing = false
			if Rij.isImageRef(value) then
				local content = images[value.image]
				if content == nil then
					pending = true
					missing = true
					result.placeholders += 1
					content = ""
				end
				value = content
			end
			if Rij.isStudioSide(node.className, property) then
				local mark = tostring(value)
				if not missing then
					sent[property] = mark
				end
				if sentBefore[property] == mark then
					continue
				end
			end
			set(instance, node, property, value)
		end
		local canvasPosition = node.properties.CanvasPosition
		if instance:IsA("ScrollingFrame") and typeof(canvasPosition) == "Vector2" and canvasPosition ~= Vector2.zero then
			table.insert(result.scrolls, { instance = instance :: Instance, position = canvasPosition })
		end
		setAttribute(instance, node, ATTR_SENT, sentText(sent))
		if instance.Name ~= node.name then
			local ok = pcall(function()
				instance.Name = node.name
			end)
			if not ok then
				set(instance, node, "Name", string.sub(node.name, 1, 100))
			end
		end
		for name, value in node.attributes do
			setAttribute(instance, node, name, value)
		end
		if node.attributes[ATTR_IMAGE] == nil then
			local image = node.properties.Image
			setAttribute(instance, node, ATTR_IMAGE, if Rij.isImageRef(image) then image.image else nil)
		end
		for _, tag in node.tags do
			if not instance:HasTag(tag) then
				local ok, err = pcall(instance.AddTag, instance, tag)
				if not ok then
					problem(node, "the tag " .. tag, err)
				end
			end
		end
		setAttribute(instance, node, ATTR_HASH, if pending then nil else node.hash)
	end

	local function fixOrder(instance: Instance, built: { Instance })
		local ordered: { Instance } = {}
		for _, child in built do
			if child:IsA("GuiObject") then
				table.insert(ordered, child)
			end
		end
		if #ordered < 2 then
			return
		end
		local position: { [Instance]: number } = {}
		for at, child in instance:GetChildren() do
			position[child] = at
		end
		local last = 0
		local sorted = true
		for _, child in ordered do
			local at = position[child] or 0
			if at < last then
				sorted = false
				break
			end
			last = at
		end
		if not sorted then
			for _, child in ordered do
				child.Parent = nil
				child.Parent = instance
			end
		end
	end

	local function build(node: Node, parent: Instance, isRoot: boolean): Instance?
		local found: Instance? = index.byKey[node.key]
		local instance: Instance
		local fresh = true
		if found ~= nil and found.ClassName == node.className then
			instance = found
			fresh = false
		else
			local ok, created = pcall(Instance.new, node.className)
			if not ok then
				problem(node, "a new " .. node.className, created)
				return nil
			end
			instance = created
			if found ~= nil then
				table.insert(replaced, { old = found, new = instance })
			end
		end
		if not fresh then
			Attributes.rename(instance)
		end
		if not fresh and instance.Parent ~= parent then
			instance.Parent = parent
		end
		if fresh then
			applyLayer(instance, node, true)
			if found ~= nil then
				result.updated += 1
			else
				result.added += 1
			end
		elseif node.hash ~= nil and instance:GetAttribute(ATTR_HASH) == node.hash then
			result.unchanged += 1
		else
			applyLayer(instance, node, false)
			result.updated += 1
		end
		setAttribute(instance, node, ATTR_ID, node.key)
		if isRoot then
			setAttribute(instance, node, ATTR_PROJECT, bundle.projectId)
		elseif instance:GetAttribute(ATTR_PROJECT) == bundle.projectId then
			setAttribute(instance, node, ATTR_PROJECT, nil)
		end

		local built: { Instance } = {}
		for _, child in node.children do
			local childInstance = build(child, instance, false)
			if childInstance then
				table.insert(built, childInstance)
			end
		end
		fixOrder(instance, built)
		if fresh then
			instance.Parent = parent
		end
		return instance
	end

	local holder: ScreenGui? = nil
	local holderParent: Instance? = nil
	local function holderFor(target: Target): ScreenGui
		local made = holder
		if made then
			return made
		end
		local holderPlan = plan.holder or { name = bundle.projectName, properties = {} }
		local screen = Instance.new("ScreenGui")
		screen.Name = holderPlan.name
		for _, property in Rij.properties("ScreenGui") do
			local value = holderPlan.properties[property]
			if value ~= nil then
				pcall(assign :: any, screen, property, value)
			end
		end
		screen:SetAttribute(ATTR_HOLDER, true)
		holder = screen
		holderParent = target.parent
		return screen
	end

	for _, target in plan.targets do
		local existing = target.existing
		local home = existing and existing.Parent
		if home and home:IsA("LayerCollector") and Attributes.get(home, "holder") == true and Attributes.get(home, "id") == nil then
			Attributes.rename(home)
		end
	end

	local rootsByParent: { [Instance]: { Instance } } = {}
	local parentOrder: { Instance } = {}
	for _, target in plan.targets do
		local existing = target.existing
		local parent: Instance
		if existing and existing.Parent then
			parent = existing.Parent :: Instance
		elseif target.inHolder then
			parent = holderFor(target)
		else
			parent = target.parent
		end
		local root = build(target.root, parent, true)
		if root then
			table.insert(result.roots, root)
			local list = rootsByParent[parent]
			if list == nil then
				list = {}
				rootsByParent[parent] = list
				table.insert(parentOrder, parent)
			end
			table.insert(list, root)
		end
	end
	for _, parent in parentOrder do
		fixOrder(parent, rootsByParent[parent])
	end
	local made = holder
	if made and holderParent then
		made.Parent = holderParent
	end

	local staleSet: { [Instance]: boolean } = {}
	for _, instance in index.stale do
		staleSet[instance] = true
	end
	for _, instance in index.stale do
		for _, child in instance:GetChildren() do
			if not index.design[child] then
				local keep = instance.Parent
				while keep and staleSet[keep] do
					keep = keep.Parent
				end
				if keep then
					child.Parent = keep
					result.moved += 1
				end
			end
		end
	end
	for _, instance in index.stale do
		local parent = instance.Parent
		if parent and not staleSet[parent] then
			instance.Parent = nil
		end
		result.removed += 1
	end

	for _, pair in replaced do
		local old, new = pair.old, pair.new
		for _, child in old:GetChildren() do
			if not index.design[child] then
				child.Parent = new
				result.moved += 1
			end
		end
		for name, value in old:GetAttributes() do
			if not Attributes.isDesigner(name) and new:GetAttribute(name) == nil then
				pcall(new.SetAttribute, new, name, value)
			end
		end
		for _, tag in old:GetTags() do
			if not new:HasTag(tag) then
				pcall(new.AddTag, new, tag)
			end
		end
		old.Parent = nil
	end
	return result
end

function Builder.settleScrolls(result: Result, set: ((Instance, Vector2) -> ())?)
	for _, item in result.scrolls do
		local instance = item.instance
		if instance.Parent ~= nil and instance:IsDescendantOf(game) then
			if set then
				set(instance, item.position)
			else
				pcall(assign :: any, instance, "CanvasPosition", item.position)
			end
		end
	end
end

return Builder
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX4D695F633D49C42245079002F1D75CC9">
			<Properties>
				<string name="Name">Bundle</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local HttpService = game:GetService("HttpService")

local Attributes = require(script.Parent.Attributes)
local Brand = require(script.Parent.Brand)
local Errors = require(script.Parent.Errors)
local Rij = require(script.Parent.Rij)
local Version = require(script.Parent.Version)
local Zip = require(script.Parent.Zip)

local Bundle = {}

Bundle.FORMAT = "nkd-bundle"
Bundle.VERSION = 1

export type Node = {
	key: string,
	className: string,
	name: string,
	properties: { [string]: any },
	attributes: { [string]: any },
	tags: { string },
	children: { Node },
	hash: string?,
	path: string,
}

export type ImageInfo = {
	hash: string,
	file: string,
	name: string,
	width: number,
	height: number,
	robloxId: number?,
	present: boolean,
}

export type Holder = {
	name: string,
	properties: { [string]: any },
}

export type Bundle = {
	version: number,
	projectId: string,
	projectName: string,
	target: string,
	roots: { Node },
	holder: Holder?,
	images: { [string]: ImageInfo },
	imageOrder: { string },
	count: number,
	warnings: { string },
	archive: Zip.Archive,
	pluginVersion: string?,
	pluginRelease: number?,
	pluginUrl: string?,
}

local ATTR_ID = Brand.attributes.id
local ATTR_PROJECT = Brand.attributes.project
local ATTR_HASH = Brand.attributes.hash

local MAX_INSTANCES = 50000
local MAX_DEPTH = 200
local MAX_BUNDLE_JSON = 64 * 1024 * 1024
local INSIDE_HELPERS: { [string]: { [string]: boolean } } = { UIStroke = { UIGradient = true } }
local SERVICES = { StarterGui = true, ReplicatedStorage = true, ServerStorage = true, Workspace = true }

local DAMAGED = "The file is damaged and can't be read. Export it again from " .. Brand.productName .. "."
local NOT_BUNDLE = "This isn't a design file. Pick a ." .. Brand.bundleExtension .. " file exported from " .. Brand.productName .. "."

local function isKey(value: any): boolean
	return type(value) == "string" and #value > 0 and #value <= 120 and string.match(value, "^[%w_%-:%.#]+$") ~= nil
end

local function isHolder(raw: any): boolean
	if type(raw) ~= "table" or raw.className ~= "ScreenGui" or raw.uid ~= nil or type(raw.attributes) ~= "table" then
		return false
	end
	local flag = Attributes.find(raw.attributes, "holder")
	return type(flag) == "table" and flag[1] == "bool" and flag[2] == true and Attributes.find(raw.attributes, "id") == nil
end

local function text(value: any, fallback: string, max: number): string
	if type(value) ~= "string" or value == "" then
		return fallback
	end
	return string.sub(value, 1, max)
end

function Bundle.parse(data: buffer): Bundle
	local archive = Zip.open(data)
	local entry = Zip.find(archive, "bundle.json")
	if entry == nil then
		for _, candidate in archive.entries do
			if string.match(candidate.name, "^[^/]+/bundle%.json$") then
				entry = candidate
				break
			end
		end
	end
	if entry == nil then
		Errors.fail(NOT_BUNDLE, "bundle: no bundle.json")
	end
	local found = entry :: Zip.Entry
	local prefix = string.sub(found.name, 1, #found.name - #"bundle.json")
	if found.size > MAX_BUNDLE_JSON then
		Errors.fail("This design is too big for the plugin to read.", "bundle: bundle.json is " .. found.size .. " bytes")
	end
	local json = buffer.tostring(Zip.read(archive, found))
	local ok, raw = pcall(HttpService.JSONDecode, HttpService, json)
	if not ok or type(raw) ~= "table" then
		Errors.fail(DAMAGED, "bundle: bundle.json is not JSON: " .. tostring(raw))
	end
	return Bundle.fromTable(raw, archive, prefix)
end

function Bundle.fromTable(raw: any, archive: Zip.Archive, prefix: string?): Bundle
	local folder = prefix or ""
	if type(raw) ~= "table" or raw.format ~= Bundle.FORMAT then
		Errors.fail(NOT_BUNDLE, "bundle: format " .. tostring(if type(raw) == "table" then raw.format else raw))
	end
	local version = raw.version
	if type(version) ~= "number" or version ~= version or version < 1 then
		Errors.fail(DAMAGED, "bundle: version " .. tostring(version))
	end
	if math.floor(version) > Bundle.VERSION then
		Errors.fail(
			string.format(
				"This design was made by a newer version of %s. Update %s: download it again from %s's export window, then import again.",
				Brand.productName,
				Brand.pluginName,
				Brand.productName
			),
			"bundle: version " .. tostring(version),
			"update"
		)
	end
	local project = if type(raw.project) == "table" then raw.project else {}
	if not isKey(project.id) then
		Errors.fail(DAMAGED, "bundle: project id " .. tostring(project.id))
	end

	local bundle: Bundle = {
		version = version,
		projectId = project.id,
		projectName = text(project.name, "Design", 200),
		target = "StarterGui",
		roots = {},
		holder = nil,
		images = {},
		imageOrder = {},
		count = 0,
		warnings = {},
		archive = archive,
	}
	local target = type(raw.target) == "table" and raw.target.service
	if type(target) == "string" and SERVICES[target] then
		bundle.target = target
	end
	local plugin = if type(raw.plugin) == "table" then raw.plugin else {}
	if Version.parse(plugin.version) then
		bundle.pluginVersion = plugin.version
	end
	bundle.pluginRelease = Version.release(plugin.release)
	local url = plugin.url
	if type(url) == "string" and #url <= 300 and string.sub(url, 1, #Brand.siteOrigin + 1) == Brand.siteOrigin .. "/" and not string.find(url, "[%s%c]") then
		bundle.pluginUrl = url
	end

	if raw.images ~= nil and type(raw.images) ~= "table" then
		Errors.fail(DAMAGED, "bundle: images is not an object")
	end
	for hash, info in raw.images or {} do
		if type(hash) ~= "string" or #hash ~= 64 or not string.match(hash, "^%x+$") or type(info) ~= "table" then
			table.insert(bundle.warnings, "An image entry is broken and was left out.")
			continue
		end
		local key = string.lower(hash)
		local file = if type(info.file) == "string" then info.file else "images/" .. key .. ".png"
		local zipEntry = Zip.find(archive, folder .. file) or Zip.find(archive, file)
		local robloxId = info.robloxId
		if type(robloxId) ~= "number" or robloxId ~= math.floor(robloxId) or robloxId <= 0 or robloxId > 2 ^ 53 then
			robloxId = nil
		end
		bundle.images[key] = {
			hash = key,
			file = if zipEntry then zipEntry.name else file,
			name = text(info.name, "Image", 100),
			width = if type(info.width) == "number" then info.width else 0,
			height = if type(info.height) == "number" then info.height else 0,
			robloxId = robloxId,
			present = zipEntry ~= nil,
		}
	end

	if type(raw.roots) ~= "table" or #raw.roots == 0 then
		Errors.fail("This design file has nothing to import.", "bundle: no roots")
	end
	local seenKeys: { [string]: boolean } = {}
	local imageSeen: { [string]: boolean } = {}
	local function note(message: string)
		table.insert(bundle.warnings, message)
	end

	local function uniqueKey(key: string): string
		if not seenKeys[key] then
			seenKeys[key] = true
			return key
		end
		local number = 2
		while seenKeys[key .. "#" .. number] do
			number += 1
		end
		local unique = key .. "#" .. number
		seenKeys[unique] = true
		return unique
	end

	local function node(rawNode: any, parentKey: string?, parentPath: string, siblingCount: { [string]: number }, depth: number): Node?
		if type(rawNode) ~= "table" then
			note("A broken object in " .. parentPath .. " was left out.")
			return nil
		end
		local className = rawNode.className
		local name = text(rawNode.name, if type(className) == "string" then className else "Object", 200)
		local path = if parentPath == "" then name else parentPath .. " > " .. name
		if not Rij.isAllowedClass(className) then
			note(path .. ": a " .. tostring(className) .. " isn't something the plugin makes, so it was left out.")
			return nil
		end
		if depth > MAX_DEPTH then
			note(path .. ": nested too deep, left out.")
			return nil
		end
		if bundle.count >= MAX_INSTANCES then
			note(path .. ": the design has too many objects; the rest were left out.")
			return nil
		end
		bundle.count += 1

		local key: string
		local attributeId = if type(rawNode.attributes) == "table" then Attributes.find(rawNode.attributes, "id") else nil
		if isKey(rawNode.uid) then
			key = rawNode.uid
		elseif type(attributeId) == "table" and attributeId[1] == "string" and isKey(attributeId[2]) then
			key = attributeId[2]
		else
			local base = if parentKey then parentKey .. ":" .. className else bundle.projectId .. ":" .. className
			local seen = (siblingCount[className] or 0) + 1
			siblingCount[className] = seen
			key = if seen == 1 then base else base .. seen
		end
		local unique = uniqueKey(key)
		if unique ~= key then
			note(path .. ": its id is used twice in the file; this copy is treated as a separate object.")
		end

		local result: Node = {
			key = unique,
			className = className,
			name = name,
			properties = {},
			attributes = {},
			tags = {},
			children = {},
			hash = nil,
			path = path,
		}

		if type(rawNode.properties) == "table" then
			for property, value in rawNode.properties do
				if type(property) ~= "string" then
					continue
				end
				local ok, decoded = Rij.decodeProperty(className, property, value)
				if ok then
					result.properties[property] = decoded
					if Rij.isImageRef(decoded) then
						local hash = decoded.image
						if not imageSeen[hash] then
							imageSeen[hash] = true
							table.insert(bundle.imageOrder, hash)
						end
					end
				else
					note(path .. ": " .. property .. " " .. tostring(decoded) .. ", so it was skipped.")
				end
			end
		end
		if type(rawNode.attributes) == "table" then
			for attribute, value in rawNode.attributes do
				if type(attribute) ~= "string" then
					continue
				end
				local newName = Attributes.current(attribute)
				if newName ~= attribute and rawNode.attributes[newName] ~= nil then
					continue
				end
				if newName == ATTR_ID or newName == ATTR_PROJECT then
					continue
				end
				local ok, decoded = Rij.decodeAttribute(newName, value)
				if newName == ATTR_HASH then
					if ok and type(decoded) == "string" then
						result.hash = decoded
					end
				elseif ok then
					result.attributes[newName] = decoded
				else
					note(path .. ": attribute " .. attribute .. " " .. tostring(decoded) .. ", so it was skipped.")
				end
			end
		end
		if type(rawNode.tags) == "table" then
			for _, tag in rawNode.tags do
				if Rij.isTag(tag) then
					if not table.find(result.tags, tag) then
						table.insert(result.tags, tag)
					end
				else
					note(path .. ": a tag that isn't valid text was skipped.")
				end
			end
		end
		if type(rawNode.children) == "table" then
			local NONE: { [string]: boolean } = {}
			local allowed: { [string]: boolean }? = if Rij.isHelper(className) then INSIDE_HELPERS[className] or NONE else nil
			local counts: { [string]: number } = {}
			local dropped = false
			for _, rawChild in rawNode.children do
				local childClass = if type(rawChild) == "table" then rawChild.className else nil
				if allowed and not (type(childClass) == "string" and allowed[childClass]) then
					dropped = true
					continue
				end
				local child = node(rawChild, unique, path, counts, depth + 1)
				if child then
					table.insert(result.children, child)
				end
			end
			if dropped then
				local can = {}
				for allowedClass in allowed or NONE do
					table.insert(can, "a " .. allowedClass)
				end
				table.sort(can)
				note(path .. ": objects inside a " .. className .. " were left out" .. (if #can > 0 then " (only " .. table.concat(can, " or ") .. " can be inside it)." else "."))
			end
		end
		return result
	end

	local rootCounts: { [string]: number } = {}
	local function addRoot(rawRoot: any)
		local className = if type(rawRoot) == "table" then rawRoot.className else nil
		if type(className) == "string" and Rij.isHelper(className) then
			note("A " .. className .. " can't be imported on its own, so it was left out.")
			return
		end
		local root = node(rawRoot, nil, "", rootCounts, 1)
		if root then
			table.insert(bundle.roots, root)
		end
	end
	for _, rawRoot in raw.roots do
		if isHolder(rawRoot) then
			local properties: { [string]: any } = {}
			if type(rawRoot.properties) == "table" then
				for property, value in rawRoot.properties do
					local ok, decoded = Rij.decodeProperty("ScreenGui", property, value)
					if ok and type(property) == "string" then
						properties[property] = decoded
					end
				end
			end
			if bundle.holder == nil then
				bundle.holder = { name = text(rawRoot.name, "UI", 100), properties = properties }
			end
			for _, rawChild in (if type(rawRoot.children) == "table" then rawRoot.children else {}) :: { any } do
				addRoot(rawChild)
			end
			continue
		end
		addRoot(rawRoot)
	end
	if #bundle.roots == 0 then
		Errors.fail("This design file has nothing the plugin can import.", "bundle: no usable roots")
	end

	for _, hash in bundle.imageOrder do
		local info = bundle.images[hash]
		if info == nil then
			bundle.images[hash] = {
				hash = hash,
				file = "",
				name = "Image",
				width = 0,
				height = 0,
				robloxId = nil,
				present = false,
			}
			table.insert(bundle.warnings, "An image is missing from the file; it will be left empty.")
		elseif not info.present and info.robloxId == nil then
			table.insert(bundle.warnings, 'The image "' .. info.name .. '" is missing from the file; it will be left empty.')
		end
	end
	return bundle
end

function Bundle.walk(nodes: { Node }, fn: (Node, Node?) -> ())
	local function visit(node: Node, parent: Node?)
		fn(node, parent)
		for _, child in node.children do
			visit(child, node)
		end
	end
	for _, root in nodes do
		visit(root, nil)
	end
end

return Bundle
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX691943FB07805A9096F1ABFFA303F58F">
			<Properties>
				<string name="Name">Checksum</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict
--!native

local band, bxor, rshift = bit32.band, bit32.bxor, bit32.rshift
local readu8 = buffer.readu8

local CRC_TABLE: { number } = table.create(256, 0)
for n = 0, 255 do
	local c = n
	for _ = 1, 8 do
		if band(c, 1) ~= 0 then
			c = bxor(0xEDB88320, rshift(c, 1))
		else
			c = rshift(c, 1)
		end
	end
	CRC_TABLE[n + 1] = c
end

local Checksum = {}

function Checksum.crc32(data: buffer, offset: number, length: number, crc: number?): number
	local c = bxor(crc or 0, 0xFFFFFFFF)
	for index = offset, offset + length - 1 do
		c = bxor(CRC_TABLE[band(bxor(c, readu8(data, index)), 0xFF) + 1], rshift(c, 8))
	end
	return bxor(c, 0xFFFFFFFF)
end

function Checksum.adler32(data: buffer, offset: number, length: number): number
	local a, b = 1, 0
	local index = offset
	local last = offset + length
	while index < last do
		local stop = math.min(index + 65536, last)
		for i = index, stop - 1 do
			a += readu8(data, i)
			b += a
		end
		a %= 65521
		b %= 65521
		index = stop
	end
	return b * 65536 + a
end

return Checksum
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBXA37C3C5CAB8A293700981BE5AEF26688">
			<Properties>
				<string name="Name">Copies</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local Copies = {}

Copies.ATTRIBUTE = "NKD_PluginVersion"
Copies.RELEASE = "NKD_PluginRelease"

function Copies.watch(place: Instance, own: Instance, version: string, release: number, changed: (others: { string }) -> ()): () -> ()
	own:SetAttribute(Copies.ATTRIBUTE, version)
	own:SetAttribute(Copies.RELEASE, release)
	local stopped = false
	local last: string? = nil
	local function check()
		if stopped then
			return
		end
		local found = {}
		for _, child in place:GetChildren() do
			if child ~= own and child.Name == own.Name then
				local value = child:GetAttribute(Copies.ATTRIBUTE)
				local counted = type(child:GetAttribute(Copies.RELEASE)) == "number"
				table.insert(found, if type(value) == "string" and counted then value else "")
			end
		end
		table.sort(found)
		local key = #found .. ":" .. table.concat(found, ",")
		if key ~= last then
			last = key
			changed(found)
		end
	end
	local connections = {
		place.ChildAdded:Connect(function()
			task.defer(check)
			task.delay(1, check)
		end),
		place.ChildRemoved:Connect(function(child: Instance)
			if child.Name == own.Name then
				check()
			end
		end),
	}
	check()
	return function()
		stopped = true
		for _, connection in connections do
			connection:Disconnect()
		end
	end
end

return Copies
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX96298698F96B4800D710483E0DAEED08">
			<Properties>
				<string name="Name">Encoding</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict
--!native

local Errors = require(script.Parent.Errors)
local Sha256 = require(script.Parent.Sha256)

local Encoding = {}

local service: any = nil
pcall(function()
	service = game:GetService("EncodingService" :: any)
end)
Encoding.native = service ~= nil

local VALUES = table.create(256, -1)
do
	local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
	for index = 1, 64 do
		VALUES[string.byte(alphabet, index) + 1] = index - 1
	end
end

local readu8, writeu8 = buffer.readu8, buffer.writeu8

function Encoding.base64DecodeLuau(text: string): buffer
	local size = #text
	if size % 4 ~= 0 then
		Errors.fail("The code is incomplete.", "base64: length " .. size .. " is not a multiple of 4")
	end
	local padding = 0
	if size > 0 and string.byte(text, size) == 61 then
		padding += 1
		if string.byte(text, size - 1) == 61 then
			padding += 1
		end
	end
	local input = buffer.fromstring(text)
	local out = buffer.create(size // 4 * 3 - padding)
	local outSize = buffer.len(out)
	local at = 0
	for index = 0, size - 1, 4 do
		local a = VALUES[readu8(input, index) + 1]
		local b = VALUES[readu8(input, index + 1) + 1]
		local c = VALUES[readu8(input, index + 2) + 1]
		local d = VALUES[readu8(input, index + 3) + 1]
		local last = index + 4 == size
		if a < 0 or b < 0 or (c < 0 and not (last and padding >= 2)) or (d < 0 and not (last and padding >= 1)) then
			Errors.fail("The code is damaged.", "base64: a character that is not base64 near " .. index)
		end
		local n = a * 262144 + b * 4096 + math.max(c, 0) * 64 + math.max(d, 0)
		writeu8(out, at, n // 65536)
		if at + 1 < outSize then
			writeu8(out, at + 1, (n // 256) % 256)
		end
		if at + 2 < outSize then
			writeu8(out, at + 2, n % 256)
		end
		at += 3
	end
	return out
end

function Encoding.base64Decode(text: string): buffer
	if Encoding.native and service then
		local ok, result = pcall(function()
			return service:Base64Decode(buffer.fromstring(text))
		end)
		if ok and typeof(result) == "buffer" then
			return result
		end
	end
	return Encoding.base64DecodeLuau(text)
end

function Encoding.sha256(data: buffer): string
	if Encoding.native and service then
		local ok, digest = pcall(function()
			return service:ComputeBufferHash(data, (Enum :: any).HashAlgorithm.Sha256)
		end)
		if ok and typeof(digest) == "buffer" and buffer.len(digest) == 32 then
			local parts = table.create(32, "")
			for index = 0, 31 do
				parts[index + 1] = string.format("%02x", readu8(digest, index))
			end
			return table.concat(parts)
		end
	end
	return Sha256.hex(data)
end

return Encoding
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX98C0ECCEE3DA50A8ED503DE93194057A">
			<Properties>
				<string name="Name">Errors</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local Errors = {}

export type Failure = {
	nkd: true,
	message: string,
	detail: string?,
	kind: string?,
}

function Errors.new(message: string, detail: string?, kind: string?): Failure
	return { nkd = true, message = message, detail = detail, kind = kind }
end

function Errors.fail(message: string, detail: string?, kind: string?): never
	error(Errors.new(message, detail, kind), 0)
end

function Errors.isFailure(value: any): boolean
	return type(value) == "table" and value.nkd == true and type(value.message) == "string"
end

function Errors.kind(value: any): string?
	if Errors.isFailure(value) and type(value.kind) == "string" then
		return value.kind
	end
	return nil
end

function Errors.message(value: any): string
	if Errors.isFailure(value) then
		return value.message
	end
	return "Something went wrong inside the plugin: " .. Errors.detail(value)
end

function Errors.detail(value: any): string
	if Errors.isFailure(value) then
		return value.detail or value.message
	end
	if type(value) == "string" then
		return value
	end
	return tostring(value)
end

function Errors.handler(value: any): any
	if Errors.isFailure(value) then
		return value
	end
	return debug.traceback(tostring(value), 2)
end

return Errors
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX62A31E0967C75A08B91EC7D799D02B70">
			<Properties>
				<string name="Name">Export</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local Attributes = require(script.Parent.Attributes)
local Brand = require(script.Parent.Brand)

local ATTR_ID = Brand.attributes.id
local OLD_ID = Brand.legacyAttributes.id
local ATTR_IMAGE = Brand.attributes.image
local OLD_IMAGE = Brand.legacyAttributes.image
local ATTR_PLUGIN = Brand.pluginAttribute
local ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"

local Export = {}

export type Prepared = {
	roots: { Instance },
	count: number,
	assigned: number,
	cleared: number,
	stamped: number,
}

function Export.stamp(): string
	return Brand.pluginVersion .. " " .. tostring(Brand.pluginRelease)
end

export type Uploads = {
	get: (self: any, hash: string) -> number?,
}

function Export.imageId(content: string): number?
	local digits = string.match(content, "^%s*rbxassetid://(%d+)%s*$") or string.match(content, "[?&][iI][dD]=(%d+)")
	return if digits then tonumber(digits) else nil
end

function Export.isUi(instance: Instance): boolean
	return instance:IsA("GuiBase2d") or instance:IsA("UIComponent")
end

function Export.newId(random: Random): string
	local chars = table.create(12, "")
	for index = 1, 12 do
		local at = random:NextInteger(1, #ALPHABET)
		chars[index] = string.sub(ALPHABET, at, at)
	end
	return "n_" .. table.concat(chars)
end

local function validId(value: any): boolean
	return type(value) == "string" and #value > 0 and #value <= 120 and string.match(value, "^[%w_%-:%.#]+$") ~= nil
end

local function isHolder(instance: Instance): boolean
	return instance:IsA("LayerCollector") and Attributes.get(instance, "holder") == true and Attributes.get(instance, "id") == nil
end

function Export.prepare(selection: { Instance }, random: Random, dryRun: boolean?, uploads: Uploads?, stamp: string?): Prepared
	local selected: { [Instance]: boolean } = {}
	for _, instance in selection do
		selected[instance] = true
	end
	local roots: { Instance } = {}
	for _, instance in selection do
		local ancestor = instance.Parent
		local inside = false
		while ancestor do
			if selected[ancestor] then
				inside = true
				break
			end
			ancestor = ancestor.Parent
		end
		if inside then
			continue
		end
		local hasUi = Export.isUi(instance)
		if not hasUi then
			for _, descendant in instance:GetDescendants() do
				if Export.isUi(descendant) then
					hasUi = true
					break
				end
			end
		end
		if hasUi then
			table.insert(roots, instance)
		end
	end

	local prepared: Prepared = { roots = roots, count = 0, assigned = 0, cleared = 0, stamped = 0 }
	local used: { [string]: boolean } = {}

	local function checkImage(instance: Instance)
		if uploads == nil or not (instance:IsA("ImageLabel") or instance:IsA("ImageButton")) then
			return
		end
		local hash: any = Attributes.get(instance, "image")
		if type(hash) ~= "string" then
			return
		end
		local uploaded = uploads:get(hash)
		local content: any = (instance :: any).Image
		if uploaded == nil or (type(content) == "string" and Export.imageId(content) == uploaded) then
			return
		end
		if not dryRun then
			instance:SetAttribute(ATTR_IMAGE, nil)
			if OLD_IMAGE ~= ATTR_IMAGE then
				instance:SetAttribute(OLD_IMAGE, nil)
			end
		end
		prepared.cleared += 1
	end

	local function setId(instance: Instance, id: string)
		local old = OLD_ID ~= ATTR_ID and instance:GetAttribute(OLD_ID) ~= nil
		if instance:GetAttribute(ATTR_ID) ~= id or old then
			if not dryRun then
				instance:SetAttribute(ATTR_ID, id)
				if old then
					instance:SetAttribute(OLD_ID, nil)
				end
			end
			prepared.assigned += 1
		end
		used[id] = true
	end

	local function helperId(parentId: string, className: string): string
		local base = parentId .. ":" .. className
		local id = base
		local number = 1
		while used[id] do
			number += 1
			id = base .. number
		end
		return id
	end

	local function visit(instance: Instance, parentId: string?, oldParentId: string?)
		if isHolder(instance) then
			for _, child in instance:GetChildren() do
				visit(child, nil, nil)
			end
			return
		end
		local ownId: string? = nil
		local oldId: string? = nil
		if Export.isUi(instance) then
			prepared.count += 1
			local current: any = Attributes.get(instance, "id")
			oldId = if validId(current) then current else nil
			local id: string
			if
				parentId
				and oldParentId
				and oldId
				and string.sub(oldId, 1, #oldParentId + 1) == oldParentId .. ":"
				and not used[parentId .. ":" .. string.sub(oldId, #oldParentId + 2)]
			then
				id = parentId .. ":" .. string.sub(oldId, #oldParentId + 2)
			elseif instance:IsA("UIComponent") and parentId then
				id = helperId(parentId, instance.ClassName)
			elseif oldId and not used[oldId] then
				id = oldId
			else
				id = Export.newId(random)
				while used[id] do
					id = Export.newId(random)
				end
			end
			setId(instance, id)
			ownId = id
			checkImage(instance)
		end
		for _, child in instance:GetChildren() do
			if ownId then
				visit(child, ownId, oldId)
			else
				visit(child, parentId, oldParentId)
			end
		end
	end
	for _, root in roots do
		visit(root, nil, nil)
		if stamp and root:GetAttribute(ATTR_PLUGIN) ~= stamp then
			if not dryRun then
				root:SetAttribute(ATTR_PLUGIN, stamp)
			end
			prepared.stamped += 1
		end
	end
	return prepared
end

function Export.fileName(roots: { Instance }): string
	local name = if #roots > 0 then roots[1].Name else "UI"
	name = string.gsub(name, '[\\/:%*%?"<>|%c]', "")
	name = string.match(name, "^%s*(.-)%s*$") or ""
	if name == "" then
		name = "UI"
	end
	return string.sub(name, 1, 80)
end

return Export
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX4AD8097C6C6EC17837FEDA91370DB3A0">
			<Properties>
				<string name="Name">History</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local ChangeHistoryService = game:GetService("ChangeHistoryService")

local Errors = require(script.Parent.Errors)

local History = {}

export type Recorder = {
	begin: (name: string, displayName: string) -> string?,
	finish: (recording: string, commit: boolean) -> (),
}

History.studio = {
	begin = function(name: string, displayName: string): string?
		return ChangeHistoryService:TryBeginRecording(name, displayName)
	end,
	finish = function(recording: string, commit: boolean)
		ChangeHistoryService:FinishRecording(
			recording,
			if commit then Enum.FinishRecordingOperation.Commit else Enum.FinishRecordingOperation.Cancel
		)
	end,
} :: Recorder

function History.run(recorder: Recorder, name: string, displayName: string, fn: () -> ()): (boolean, any)
	local recording = recorder.begin(name, displayName)
	if recording == nil then
		return false, Errors.new("Studio is busy with another change. Try again in a moment.", "history: no recording")
	end
	local ok, err = xpcall(fn :: any, Errors.handler)
	recorder.finish(recording, ok)
	return ok, err
end

return History
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX8FE7766A30E7828AA1D2FB47D564BAF6">
			<Properties>
				<string name="Name">Images</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local Bundle = require(script.Parent.Bundle)
local Encoding = require(script.Parent.Encoding)
local Errors = require(script.Parent.Errors)
local Png = require(script.Parent.Png)
local Zip = require(script.Parent.Zip)

local Images = {}

Images.MAX_SIZE = 1024
Images.RATE_LIMIT = 60
Images.RATE_WINDOW = 60
Images.RETRIES = 2

export type Creator = {
	kind: string,
	id: number,
}

export type UploadError = {
	kind: string,
	message: string,
}

export type Uploader = {
	check: (self: any) -> (boolean, string?),
	upload: (self: any, image: Png.Image, name: string, description: string, creator: Creator) -> (number?, UploadError?),
}

export type Cache = {
	get: (self: any, hash: string) -> number?,
	set: (self: any, hash: string, id: number) -> (),
}

export type Needs = {
	known: { [string]: string },
	upload: { string },
	missing: { string },
}

export type Resolution = {
	contents: { [string]: string },
	uploaded: number,
	reused: number,
	missing: number,
	blocked: UploadError?,
	errors: { string },
	cancelled: boolean,
}

export type Options = {
	uploader: Uploader,
	cache: Cache,
	creator: Creator,
	description: string,
	allowUpload: boolean,
	isCancelled: () -> boolean,
	progress: (done: number, total: number, text: string) -> (),
	clock: () -> number,
	wait: (seconds: number) -> (),
}

local function content(id: number): string
	return "rbxassetid://" .. string.format("%d", id)
end

local function nameCharacter(code: number): boolean
	return (code >= 48 and code <= 57)
		or (code >= 65 and code <= 90)
		or (code >= 97 and code <= 122)
		or code == 32
		or code == 45
		or code == 95
		or code == 46
		or code == 40
		or code == 41
		or (code >= 0xC0 and code < 0x2000)
		or (code >= 0x3040 and code < 0xE000)
		or (code >= 0xF900 and code < 0xFE00)
		or (code >= 0xFF10 and code < 0xFFF0)
end

function Images.assetName(name: string): string
	local characters: { string } = {}
	local ok = pcall(function()
		for _, code in utf8.codes(name) do
			if code == 9 or code == 10 or code == 13 then
				code = 32
			end
			if nameCharacter(code) then
				table.insert(characters, utf8.char(code))
			end
		end
	end)
	if not ok then
		characters = {}
		for character in string.gmatch(name, "[%w %-_%.%(%)]") do
			table.insert(characters, character)
		end
	end
	local clean = string.gsub(table.concat(characters), "%s+", " ")
	clean = string.match(clean, "^%s*(.-)%s*$") or ""
	if clean == "" then
		return "Image"
	end
	local short = {}
	for _, code in utf8.codes(clean) do
		if #short == 50 then
			break
		end
		table.insert(short, utf8.char(code))
	end
	return (string.gsub(table.concat(short), "%s+$", ""))
end

function Images.needs(bundle: Bundle.Bundle, cache: Cache): Needs
	local needs: Needs = { known = {}, upload = {}, missing = {} }
	for _, hash in bundle.imageOrder do
		local info = bundle.images[hash]
		local cached = cache:get(hash)
		if info and info.robloxId then
			needs.known[hash] = content(info.robloxId)
		elseif cached then
			needs.known[hash] = content(cached)
		elseif info and info.present then
			table.insert(needs.upload, hash)
		else
			table.insert(needs.missing, hash)
		end
	end
	return needs
end

function Images.load(bundle: Bundle.Bundle, hash: string): Png.Image
	local info = bundle.images[hash]
	local entry = info and Zip.find(bundle.archive, info.file)
	if info == nil or entry == nil then
		Errors.fail("An image is missing from the file.", "images: no entry for " .. hash)
	end
	local bytes = Zip.read(bundle.archive, entry :: Zip.Entry)
	if Encoding.sha256(bytes) ~= hash then
		Errors.fail(
			'The image "' .. (info :: Bundle.ImageInfo).name .. '" in the file is damaged. Export the design again.',
			"images: SHA-256 mismatch for " .. hash
		)
	end
	local image = Png.decode(bytes)
	if image.width > Images.MAX_SIZE or image.height > Images.MAX_SIZE then
		Errors.fail(
			string.format(
				'The image "%s" is %d x %d; Roblox takes at most %d x %d from a plugin.',
				(info :: Bundle.ImageInfo).name,
				image.width,
				image.height,
				Images.MAX_SIZE,
				Images.MAX_SIZE
			),
			"images: too big " .. hash
		)
	end
	return image
end

function Images.resolve(bundle: Bundle.Bundle, options: Options): Resolution
	local needs = Images.needs(bundle, options.cache)
	local resolution: Resolution = {
		contents = {},
		uploaded = 0,
		reused = 0,
		missing = #needs.missing,
		blocked = nil,
		errors = {},
		cancelled = false,
	}
	for hash, value in needs.known do
		resolution.contents[hash] = value
		resolution.reused += 1
	end
	local total = #needs.upload
	if total == 0 then
		return resolution
	end
	if not options.allowUpload then
		resolution.missing += total
		return resolution
	end
	local ok, reason = options.uploader:check()
	if not ok then
		resolution.blocked = { kind = "blocked", message = reason or "Uploads are not available." }
		resolution.missing += total
		return resolution
	end

	local recent: { number } = {}
	local function waitForLimit(done: number)
		while true do
			local now = options.clock()
			while #recent > 0 and now - recent[1] >= Images.RATE_WINDOW do
				table.remove(recent, 1)
			end
			if #recent < Images.RATE_LIMIT or options.isCancelled() then
				return
			end
			local seconds = math.ceil(Images.RATE_WINDOW - (now - recent[1]))
			options.progress(done, total, string.format("Roblox allows %d uploads a minute. Going on in %d s…", Images.RATE_LIMIT, seconds))
			options.wait(1)
		end
	end

	for done, hash in needs.upload do
		if options.isCancelled() then
			resolution.cancelled = true
			resolution.missing += total - done + 1
			break
		end
		local info = bundle.images[hash]
		options.progress(done - 1, total, string.format('Uploading "%s" (%d of %d)', info.name, done, total))
		options.wait(0)
		local loaded, image = xpcall(Images.load, Errors.handler, bundle, hash)
		if not loaded then
			table.insert(resolution.errors, Errors.message(image))
			resolution.missing += 1
			continue
		end
		local id: number? = nil
		local failure: UploadError? = nil
		for attempt = 0, Images.RETRIES do
			waitForLimit(done - 1)
			if options.isCancelled() then
				break
			end
			table.insert(recent, options.clock())
			id, failure = options.uploader:upload(image, Images.assetName(info.name), options.description, options.creator)
			if id or failure == nil or failure.kind == "blocked" or attempt == Images.RETRIES then
				break
			end
			local pause = if failure.kind == "limit" then 20 else 5 * (attempt + 1)
			options.progress(done - 1, total, string.format('Retrying "%s" in %d s…', info.name, pause))
			options.wait(pause)
		end
		if id then
			options.cache:set(hash, id)
			resolution.contents[hash] = content(id)
			resolution.uploaded += 1
			options.progress(done, total, string.format('Uploaded "%s" (%d of %d)', info.name, done, total))
		elseif failure and failure.kind == "blocked" then
			resolution.blocked = failure
			resolution.missing += total - done + 1
			break
		elseif options.isCancelled() then
			resolution.cancelled = true
			resolution.missing += total - done + 1
			break
		else
			resolution.missing += 1
			table.insert(
				resolution.errors,
				string.format('"%s" could not be uploaded: %s', info.name, if failure then failure.message else "no answer from Roblox")
			)
		end
	end
	return resolution
end

return Images
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBXA45B413BF048C9861AC91F54714518E7">
			<Properties>
				<string name="Name">Inflate</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict
--!native

local Brand = require(script.Parent.Brand)
local Checksum = require(script.Parent.Checksum)
local Errors = require(script.Parent.Errors)

local band, bor, bxor, lshift, rshift = bit32.band, bit32.bor, bit32.bxor, bit32.lshift, bit32.rshift
local readu8, writeu8, bcopy = buffer.readu8, buffer.writeu8, buffer.copy

local FAST_BITS = 10
local FAST_SIZE = 1024
local FAST_MASK = 1023

local LENGTH_BASE = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 }
local LENGTH_EXTRA = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }
local DIST_BASE = {
	1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
	8193, 12289, 16385, 24577,
}
local DIST_EXTRA = { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 }
local CODE_LENGTH_ORDER = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }

local DAMAGED = "The file is damaged and can't be read. Export it again from " .. Brand.productName .. "."

type Huffman = {
	fast: { number },
	counts: { number },
	symbols: { number },
}

type State = {
	src: buffer,
	pos: number,
	stop: number,
	bitbuf: number,
	bitcnt: number,
	padded: number,
}

local function fail(detail: string): never
	return Errors.fail(DAMAGED, "deflate: " .. detail)
end

local function build(lengths: { number }, first: number, count: number): Huffman
	local counts = table.create(15, 0)
	for symbol = 0, count - 1 do
		local length = lengths[first + symbol]
		if length > 0 then
			counts[length] += 1
		end
	end
	local left = 1
	for length = 1, 15 do
		left = left * 2 - counts[length]
		if left < 0 then
			fail("over-subscribed code")
		end
	end
	local offsets = table.create(16, 0)
	for length = 1, 15 do
		offsets[length + 1] = offsets[length] + counts[length]
	end
	local symbols = table.create(math.max(count, 1), 0)
	for symbol = 0, count - 1 do
		local length = lengths[first + symbol]
		if length > 0 then
			offsets[length] += 1
			symbols[offsets[length]] = symbol
		end
	end
	local fast = table.create(FAST_SIZE, 0)
	local code = 0
	local index = 1
	for length = 1, 15 do
		for _ = 1, counts[length] do
			if length <= FAST_BITS then
				local reversed = 0
				local value = code
				for _ = 1, length do
					reversed = reversed * 2 + band(value, 1)
					value = rshift(value, 1)
				end
				local entry = symbols[index] * 16 + length
				local step = lshift(1, length)
				for slot = reversed, FAST_SIZE - 1, step do
					fast[slot + 1] = entry
				end
			end
			code += 1
			index += 1
		end
		code *= 2
	end
	return { fast = fast, counts = counts, symbols = symbols }
end

local function bits(s: State, n: number): number
	while s.bitcnt < n do
		local byte = 0
		if s.pos < s.stop then
			byte = readu8(s.src, s.pos)
		else
			s.padded += 1
		end
		s.pos += 1
		s.bitbuf = bor(s.bitbuf, lshift(byte, s.bitcnt))
		s.bitcnt += 8
	end
	local value = band(s.bitbuf, lshift(1, n) - 1)
	s.bitbuf = rshift(s.bitbuf, n)
	s.bitcnt -= n
	return value
end

local function decodeSymbol(s: State, h: Huffman): number
	while s.bitcnt <= 24 do
		local byte = 0
		if s.pos < s.stop then
			byte = readu8(s.src, s.pos)
		else
			s.padded += 1
		end
		s.pos += 1
		s.bitbuf = bor(s.bitbuf, lshift(byte, s.bitcnt))
		s.bitcnt += 8
	end
	local entry = h.fast[band(s.bitbuf, FAST_MASK) + 1]
	if entry > 0 then
		local length = band(entry, 15)
		s.bitbuf = rshift(s.bitbuf, length)
		s.bitcnt -= length
		return rshift(entry, 4)
	end
	local code, firstCode, index = 0, 0, 0
	local counts = h.counts
	for length = 1, 15 do
		code = bor(code, band(s.bitbuf, 1))
		s.bitbuf = rshift(s.bitbuf, 1)
		s.bitcnt -= 1
		local count = counts[length]
		if code - count < firstCode then
			return h.symbols[index + code - firstCode + 1]
		end
		index += count
		firstCode = (firstCode + count) * 2
		code *= 2
	end
	return fail("invalid code")
end

local FIXED_LITERALS: Huffman, FIXED_DISTANCES: Huffman
do
	local lengths = table.create(288, 8)
	for symbol = 144, 255 do
		lengths[symbol + 1] = 9
	end
	for symbol = 256, 279 do
		lengths[symbol + 1] = 7
	end
	FIXED_LITERALS = build(lengths, 1, 288)
	FIXED_DISTANCES = build(table.create(30, 5), 1, 30)
end

local function readDynamic(s: State): (Huffman, Huffman)
	local literalCount = bits(s, 5) + 257
	local distanceCount = bits(s, 5) + 1
	local codeCount = bits(s, 4) + 4
	if literalCount > 286 or distanceCount > 30 then
		fail("too many codes")
	end
	local codeLengths = table.create(19, 0)
	for index = 1, codeCount do
		codeLengths[CODE_LENGTH_ORDER[index] + 1] = bits(s, 3)
	end
	local lengthCode = build(codeLengths, 1, 19)
	local total = literalCount + distanceCount
	local lengths = table.create(total, 0)
	local index = 1
	while index <= total do
		local symbol = decodeSymbol(s, lengthCode)
		if symbol < 16 then
			lengths[index] = symbol
			index += 1
		else
			local value, repeats = 0, 0
			if symbol == 16 then
				if index == 1 then
					fail("repeat without a length")
				end
				value = lengths[index - 1]
				repeats = 3 + bits(s, 2)
			elseif symbol == 17 then
				repeats = 3 + bits(s, 3)
			else
				repeats = 11 + bits(s, 7)
			end
			if index + repeats - 1 > total then
				fail("too many code lengths")
			end
			for _ = 1, repeats do
				lengths[index] = value
				index += 1
			end
		end
	end
	if lengths[257] == 0 then
		fail("no end-of-block code")
	end
	return build(lengths, 1, literalCount), build(lengths, literalCount + 1, distanceCount)
end

local function storedBlock(s: State, out: buffer, outPos: number, outEnd: number): number
	local drop = s.bitcnt % 8
	s.bitbuf = rshift(s.bitbuf, drop)
	s.bitcnt -= drop
	local length = bits(s, 16)
	local check = bits(s, 16)
	if bxor(length, 0xFFFF) ~= check then
		fail("stored block length check")
	end
	s.pos -= s.bitcnt // 8
	s.bitbuf = 0
	s.bitcnt = 0
	if s.pos > s.stop or s.pos + length > s.stop then
		fail("stored block runs past the end")
	end
	s.padded = 0
	if outPos + length > outEnd then
		fail("more data than expected")
	end
	bcopy(out, outPos, s.src, s.pos, length)
	s.pos += length
	return outPos + length
end

local function decodeBlock(s: State, out: buffer, outPos: number, outEnd: number, lit: Huffman, dist: Huffman): number
	local src, pos, stop = s.src, s.pos, s.stop
	local bitbuf, bitcnt, padded = s.bitbuf, s.bitcnt, s.padded
	local litFast, litCounts, litSymbols = lit.fast, lit.counts, lit.symbols
	local distFast, distCounts, distSymbols = dist.fast, dist.counts, dist.symbols
	while true do
		while bitcnt <= 24 do
			if pos < stop then
				bitbuf = bor(bitbuf, lshift(readu8(src, pos), bitcnt))
			else
				padded += 1
				if padded > 8 then
					fail("ends too early")
				end
			end
			pos += 1
			bitcnt += 8
		end
		local symbol: number
		local entry = litFast[band(bitbuf, FAST_MASK) + 1]
		if entry > 0 then
			local length = band(entry, 15)
			symbol = rshift(entry, 4)
			bitbuf = rshift(bitbuf, length)
			bitcnt -= length
		else
			symbol = -1
			local code, firstCode, index = 0, 0, 0
			for length = 1, 15 do
				code = bor(code, band(bitbuf, 1))
				bitbuf = rshift(bitbuf, 1)
				bitcnt -= 1
				local count = litCounts[length]
				if code - count < firstCode then
					symbol = litSymbols[index + code - firstCode + 1]
					break
				end
				index += count
				firstCode = (firstCode + count) * 2
				code *= 2
			end
			if symbol < 0 then
				fail("invalid literal code")
			end
		end

		if symbol < 256 then
			if outPos >= outEnd then
				fail("more data than expected")
			end
			writeu8(out, outPos, symbol)
			outPos += 1
		elseif symbol == 256 then
			break
		else
			symbol -= 257
			if symbol >= 29 then
				fail("invalid length code")
			end
			local length = LENGTH_BASE[symbol + 1]
			local extra = LENGTH_EXTRA[symbol + 1]
			if extra > 0 then
				length += band(bitbuf, lshift(1, extra) - 1)
				bitbuf = rshift(bitbuf, extra)
				bitcnt -= extra
			end

			while bitcnt <= 24 do
				if pos < stop then
					bitbuf = bor(bitbuf, lshift(readu8(src, pos), bitcnt))
				else
					padded += 1
					if padded > 8 then
						fail("ends too early")
					end
				end
				pos += 1
				bitcnt += 8
			end
			local distSymbol: number
			local distEntry = distFast[band(bitbuf, FAST_MASK) + 1]
			if distEntry > 0 then
				local codeLength = band(distEntry, 15)
				distSymbol = rshift(distEntry, 4)
				bitbuf = rshift(bitbuf, codeLength)
				bitcnt -= codeLength
			else
				distSymbol = -1
				local code, firstCode, index = 0, 0, 0
				for codeLength = 1, 15 do
					code = bor(code, band(bitbuf, 1))
					bitbuf = rshift(bitbuf, 1)
					bitcnt -= 1
					local count = distCounts[codeLength]
					if code - count < firstCode then
						distSymbol = distSymbols[index + code - firstCode + 1]
						break
					end
					index += count
					firstCode = (firstCode + count) * 2
					code *= 2
				end
				if distSymbol < 0 then
					fail("invalid distance code")
				end
			end
			if distSymbol >= 30 then
				fail("invalid distance code")
			end
			local distance = DIST_BASE[distSymbol + 1]
			local distExtra = DIST_EXTRA[distSymbol + 1]
			if distExtra > 0 then
				while bitcnt < distExtra do
					if pos < stop then
						bitbuf = bor(bitbuf, lshift(readu8(src, pos), bitcnt))
					else
						padded += 1
						if padded > 8 then
							fail("ends too early")
						end
					end
					pos += 1
					bitcnt += 8
				end
				distance += band(bitbuf, lshift(1, distExtra) - 1)
				bitbuf = rshift(bitbuf, distExtra)
				bitcnt -= distExtra
			end

			if distance > outPos then
				fail("distance too far back")
			end
			if outPos + length > outEnd then
				fail("more data than expected")
			end
			local from = outPos - distance
			if length < 16 then
				for offset = 0, length - 1 do
					writeu8(out, outPos + offset, readu8(out, from + offset))
				end
				outPos += length
			elseif distance >= length then
				bcopy(out, outPos, out, from, length)
				outPos += length
			else
				local remaining = length
				while remaining > 0 do
					local chunk = math.min(remaining, outPos - from)
					bcopy(out, outPos, out, from, chunk)
					outPos += chunk
					remaining -= chunk
				end
			end
		end
	end
	s.pos, s.bitbuf, s.bitcnt, s.padded = pos, bitbuf, bitcnt, padded
	return outPos
end

local Inflate = {}

function Inflate.raw(src: buffer, offset: number, length: number, outSize: number): (buffer, number)
	if offset < 0 or length < 0 or offset + length > buffer.len(src) then
		fail("bad input range")
	end
	local s: State = { src = src, pos = offset, stop = offset + length, bitbuf = 0, bitcnt = 0, padded = 0 }
	local out = buffer.create(outSize)
	local outPos = 0
	while true do
		local final = bits(s, 1)
		local kind = bits(s, 2)
		if kind == 0 then
			outPos = storedBlock(s, out, outPos, outSize)
		elseif kind == 1 then
			outPos = decodeBlock(s, out, outPos, outSize, FIXED_LITERALS, FIXED_DISTANCES)
		elseif kind == 2 then
			local literals, distances = readDynamic(s)
			outPos = decodeBlock(s, out, outPos, outSize, literals, distances)
		else
			fail("invalid block type")
		end
		if s.padded * 8 > s.bitcnt then
			fail("ends too early")
		end
		if final == 1 then
			break
		end
	end
	if outPos ~= outSize then
		fail(string.format("%d bytes instead of %d", outPos, outSize))
	end
	local used = s.pos - offset - s.bitcnt // 8
	return out, used
end

function Inflate.zlib(src: buffer, offset: number, length: number, outSize: number): buffer
	if length < 6 then
		fail("zlib stream too short")
	end
	local cmf, flg = readu8(src, offset), readu8(src, offset + 1)
	if band(cmf, 15) ~= 8 or rshift(cmf, 4) > 7 or (cmf * 256 + flg) % 31 ~= 0 then
		fail("not a zlib stream")
	end
	if band(flg, 0x20) ~= 0 then
		fail("zlib preset dictionary")
	end
	local out, used = Inflate.raw(src, offset + 2, length - 2, outSize)
	local checkAt = offset + 2 + used
	if checkAt + 4 > offset + length then
		fail("zlib checksum missing")
	end
	local expected = bit32.byteswap(buffer.readu32(src, checkAt))
	if Checksum.adler32(out, 0, outSize) ~= expected then
		fail("zlib checksum mismatch")
	end
	return out
end

return Inflate
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX59207BE157D8D98612D33054CFA214A4">
			<Properties>
				<string name="Name">Paste</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local Brand = require(script.Parent.Brand)
local Encoding = require(script.Parent.Encoding)
local Errors = require(script.Parent.Errors)

local Paste = {}

local NAMES: { string } = {}
local NEWEST: { [string]: number } = {}
local READS: { [string]: boolean } = {}
for _, prefix in { Brand.pastePrefix, table.unpack(Brand.legacyPastePrefixes) } do
	local name = string.match(prefix, "^(%a+)") or prefix
	local version = tonumber(string.match(prefix, "(%d+)$")) or 1
	if NEWEST[name] == nil then
		table.insert(NAMES, name)
	end
	NEWEST[name] = math.max(NEWEST[name] or 0, version)
	READS[name .. version] = true
end
local MAX_PARTS = 999

export type Part = {
	part: number,
	parts: number,
	id: string,
	data: string,
}

export type Collector = {
	id: string?,
	parts: number,
	data: { [number]: string },
}

export type Status = {
	complete: boolean,
	have: number,
	need: number,
	missing: { number },
	restarted: boolean,
}

local TOOL = Brand.productName
local NOT_A_CODE = "This isn't a code from " .. TOOL .. ". In " .. TOOL .. ", click Export, then Copy for the Studio plugin, and paste it here."
local DAMAGED = "This code is damaged. Copy it again in " .. TOOL .. " and paste it here."

local function nextCode(text: string, from: number): (number?, string?)
	local first: number?, firstName: string? = nil, nil
	for _, name in NAMES do
		local at = string.find(text, name .. "%d+:", from)
		if at and (first == nil or at < first) then
			first, firstName = at, name
		end
	end
	return first, firstName
end

function Paste.find(text: string): { Part }
	local clean = string.gsub(text, "%s+", "")
	local found: { Part } = {}
	local start, name = nextCode(clean, 1)
	if start == nil then
		Errors.fail(NOT_A_CODE, "paste: no " .. table.concat(NAMES, " or ") .. " code")
	end
	while start and name do
		local nextStart, nextName = nextCode(clean, start + #name)
		local code = string.sub(clean, start, if nextStart then nextStart - 1 else #clean)
		local versionText = string.match(code, "^" .. name .. "(%d+):")
		local version = if versionText then tonumber(versionText) else nil
		if version and version > (NEWEST[name] or 0) then
			Errors.fail(
				string.format(
					"This code was made by a newer version of %s. Update %s: download it again from %s's export window.",
					TOOL,
					Brand.pluginName,
					TOOL
				),
				"paste: version " .. tostring(version),
				"update"
			)
		end
		local part, parts, id, data = string.match(code, "^" .. name .. "%d+:(%d+)/(%d+):(%x+):([%w%+/=]*)$")
		local partNumber, partCount = tonumber(part), tonumber(parts)
		if version == nil or not READS[name .. version] or partNumber == nil or partCount == nil or id == nil or data == nil then
			Errors.fail(DAMAGED, "paste: bad code " .. string.sub(code, 1, 40))
		end
		local number, count = partNumber :: number, partCount :: number
		if #(id :: string) ~= 8 or number < 1 or count < 1 or number > count or count > MAX_PARTS or data == "" then
			Errors.fail(DAMAGED, "paste: bad header " .. string.sub(code, 1, 40))
		end
		table.insert(found, { part = number, parts = count, id = string.lower(id :: string), data = data :: string })
		start, name = nextStart, nextName
	end
	return found
end

function Paste.collector(): Collector
	return { id = nil, parts = 0, data = {} }
end

local function status(collector: Collector, restarted: boolean): Status
	local missing = {}
	local have = 0
	for number = 1, collector.parts do
		if collector.data[number] then
			have += 1
		else
			table.insert(missing, number)
		end
	end
	return { complete = collector.parts > 0 and #missing == 0, have = have, need = collector.parts, missing = missing, restarted = restarted }
end

function Paste.add(collector: Collector, parts: { Part }): Status
	local restarted = false
	for _, part in parts do
		if collector.id ~= part.id or collector.parts ~= part.parts then
			restarted = restarted or (collector.id ~= nil and next(collector.data) ~= nil)
			collector.id = part.id
			collector.parts = part.parts
			collector.data = {}
		end
		collector.data[part.part] = part.data
	end
	return status(collector, restarted)
end

function Paste.assemble(collector: Collector): buffer
	local pieces = table.create(collector.parts, "")
	for number = 1, collector.parts do
		local piece = collector.data[number]
		if piece == nil then
			Errors.fail("Part " .. number .. " of the code is still missing.", "paste: part " .. number .. " missing")
		end
		pieces[number] = piece :: string
	end
	local zip = Encoding.base64Decode(table.concat(pieces))
	if string.sub(Encoding.sha256(zip), 1, 8) ~= collector.id then
		Errors.fail(
			"The code is incomplete or damaged (maybe it was cut off when pasting). Copy it again in " .. TOOL .. " and paste it here.",
			"paste: SHA-256 does not match the id " .. tostring(collector.id)
		)
	end
	return zip
end

function Paste.describe(state: Status): string
	local names = {}
	for _, number in state.missing do
		table.insert(names, "part " .. number)
	end
	local list = if #names <= 1 then names[1] or "" else table.concat(names, ", ", 1, #names - 1) .. " and " .. names[#names]
	return string.format("%d of %d parts pasted. Still needed: %s.", state.have, state.need, list)
end

return Paste
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX46F42BD0E23536BDBB2EAF02D02DF4AD">
			<Properties>
				<string name="Name">Png</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict
--!native

local Brand = require(script.Parent.Brand)
local Checksum = require(script.Parent.Checksum)
local Errors = require(script.Parent.Errors)
local Inflate = require(script.Parent.Inflate)

local band, rshift = bit32.band, bit32.rshift
local readu8, writeu8 = buffer.readu8, buffer.writeu8

export type Image = {
	width: number,
	height: number,
	pixels: buffer,
}

export type Info = {
	width: number,
	height: number,
	bitDepth: number,
	colorType: number,
	interlace: number,
}

local MAX_SIDE = 16384
local MAX_PIXELS = 4096 * 4096
local SIGNATURE = "\137PNG\r\n\26\n"
local DAMAGED = "An image in the file is damaged. Export the design again from " .. Brand.productName .. "."

local CHANNELS = { [0] = 1, [2] = 3, [3] = 1, [4] = 2, [6] = 4 }
local DEPTHS = {
	[0] = { [1] = true, [2] = true, [4] = true, [8] = true, [16] = true },
	[2] = { [8] = true, [16] = true },
	[3] = { [1] = true, [2] = true, [4] = true, [8] = true },
	[4] = { [8] = true, [16] = true },
	[6] = { [8] = true, [16] = true },
}

local function fail(detail: string): never
	return Errors.fail(DAMAGED, "png: " .. detail)
end

local function u32be(data: buffer, offset: number): number
	return bit32.byteswap(buffer.readu32(data, offset))
end

local function u16be(data: buffer, offset: number): number
	return readu8(data, offset) * 256 + readu8(data, offset + 1)
end

local Png = {}

function Png.info(data: buffer): Info
	if buffer.len(data) < 33 or buffer.readstring(data, 0, 8) ~= SIGNATURE then
		fail("not a PNG")
	end
	if u32be(data, 8) ~= 13 or buffer.readstring(data, 12, 4) ~= "IHDR" then
		fail("no header")
	end
	return {
		width = u32be(data, 16),
		height = u32be(data, 20),
		bitDepth = readu8(data, 24),
		colorType = readu8(data, 25),
		interlace = readu8(data, 28),
	}
end

local function unfilter(raw: buffer, height: number, rowBytes: number, bpp: number)
	local stride = rowBytes + 1
	for y = 0, height - 1 do
		local filterAt = y * stride
		local first = filterAt + 1
		local last = first + rowBytes - 1
		local filter = readu8(raw, filterAt)
		if filter == 0 then
			continue
		elseif filter == 1 then
			for i = first + bpp, last do
				writeu8(raw, i, band(readu8(raw, i) + readu8(raw, i - bpp), 255))
			end
		elseif filter == 2 then
			if y > 0 then
				for i = first, last do
					writeu8(raw, i, band(readu8(raw, i) + readu8(raw, i - stride), 255))
				end
			end
		elseif filter == 3 then
			if y > 0 then
				for i = first, math.min(first + bpp - 1, last) do
					writeu8(raw, i, band(readu8(raw, i) + rshift(readu8(raw, i - stride), 1), 255))
				end
				for i = first + bpp, last do
					writeu8(raw, i, band(readu8(raw, i) + rshift(readu8(raw, i - bpp) + readu8(raw, i - stride), 1), 255))
				end
			else
				for i = first + bpp, last do
					writeu8(raw, i, band(readu8(raw, i) + rshift(readu8(raw, i - bpp), 1), 255))
				end
			end
		elseif filter == 4 then
			if y > 0 then
				for i = first, math.min(first + bpp - 1, last) do
					writeu8(raw, i, band(readu8(raw, i) + readu8(raw, i - stride), 255))
				end
				for i = first + bpp, last do
					local a = readu8(raw, i - bpp)
					local b = readu8(raw, i - stride)
					local c = readu8(raw, i - stride - bpp)
					local p = a + b - c
					local pa = math.abs(p - a)
					local pb = math.abs(p - b)
					local pc = math.abs(p - c)
					local predictor = if pa <= pb and pa <= pc then a elseif pb <= pc then b else c
					writeu8(raw, i, band(readu8(raw, i) + predictor, 255))
				end
			else
				for i = first + bpp, last do
					writeu8(raw, i, band(readu8(raw, i) + readu8(raw, i - bpp), 255))
				end
			end
		else
			fail("unknown row filter " .. filter)
		end
	end
end

function Png.decode(data: buffer): Image
	local size = buffer.len(data)
	local info = Png.info(data)
	local width, height, depth, colorType = info.width, info.height, info.bitDepth, info.colorType
	if width < 1 or height < 1 or width > MAX_SIDE or height > MAX_SIDE or width * height > MAX_PIXELS then
		Errors.fail("An image in the file is too big to read (" .. width .. " x " .. height .. ").", "png: size")
	end
	if CHANNELS[colorType] == nil or not DEPTHS[colorType][depth] then
		fail("color type " .. colorType .. " with " .. depth .. " bits")
	end
	if readu8(data, 26) ~= 0 or readu8(data, 27) ~= 0 then
		fail("unknown compression or filter method")
	end
	if info.interlace ~= 0 then
		Errors.fail(
			"An image in the file is interlaced, which " .. Brand.pluginName .. " can't read. Export the design again from " .. Brand.productName .. ".",
			"png: interlaced"
		)
	end

	local palette: buffer? = nil
	local paletteCount = 0
	local keyGray, keyRed, keyGreen, keyBlue = -1, -1, -1, -1
	local parts: { { number } } = {}
	local compressedSize = 0
	local pos = 8
	local ended = false
	while pos + 12 <= size do
		local length = u32be(data, pos)
		if length > size - pos - 12 then
			fail("chunk runs past the end")
		end
		local chunkType = buffer.readstring(data, pos + 4, 4)
		local body = pos + 8
		if Checksum.crc32(data, pos + 4, length + 4) ~= u32be(data, body + length) then
			fail("checksum of " .. chunkType)
		end
		if chunkType == "PLTE" then
			if length % 3 ~= 0 or length == 0 or length > 768 then
				fail("palette size")
			end
			local entries = buffer.create(1024)
			paletteCount = length // 3
			for index = 0, paletteCount - 1 do
				writeu8(entries, index * 4, readu8(data, body + index * 3))
				writeu8(entries, index * 4 + 1, readu8(data, body + index * 3 + 1))
				writeu8(entries, index * 4 + 2, readu8(data, body + index * 3 + 2))
				writeu8(entries, index * 4 + 3, 255)
			end
			palette = entries
		elseif chunkType == "tRNS" then
			if colorType == 3 then
				local entries = palette
				if entries == nil or length > paletteCount then
					fail("transparency before or longer than the palette")
				else
					for index = 0, length - 1 do
						writeu8(entries, index * 4 + 3, readu8(data, body + index))
					end
				end
			elseif colorType == 0 and length >= 2 then
				keyGray = u16be(data, body)
			elseif colorType == 2 and length >= 6 then
				keyRed, keyGreen, keyBlue = u16be(data, body), u16be(data, body + 2), u16be(data, body + 4)
			end
		elseif chunkType == "IDAT" then
			table.insert(parts, { body, length })
			compressedSize += length
		elseif chunkType == "IEND" then
			ended = true
			break
		elseif chunkType ~= "IHDR" and band(readu8(data, pos + 4), 0x20) == 0 then
			fail("unknown required chunk " .. chunkType)
		end
		pos = body + length + 4
	end
	if not ended or #parts == 0 then
		fail("image data missing")
	end
	if colorType == 3 and palette == nil then
		fail("palette missing")
	end

	local channels = CHANNELS[colorType]
	local bitsPerPixel = channels * depth
	local rowBytes = (width * bitsPerPixel + 7) // 8
	local bpp = math.max(1, bitsPerPixel // 8)
	local rawSize = height * (rowBytes + 1)
	local raw: buffer
	if #parts == 1 then
		raw = Inflate.zlib(data, parts[1][1], parts[1][2], rawSize)
	else
		local joined = buffer.create(compressedSize)
		local at = 0
		for _, part in parts do
			buffer.copy(joined, at, data, part[1], part[2])
			at += part[2]
		end
		raw = Inflate.zlib(joined, 0, compressedSize, rawSize)
	end
	unfilter(raw, height, rowBytes, bpp)

	local pixels = buffer.create(width * height * 4)
	local stride = rowBytes + 1
	for y = 0, height - 1 do
		local row = y * stride + 1
		local out = y * width * 4
		if colorType == 6 and depth == 8 then
			buffer.copy(pixels, out, raw, row, width * 4)
		elseif colorType == 2 and depth == 8 then
			for x = 0, width - 1 do
				local i = row + x * 3
				local r, g, b = readu8(raw, i), readu8(raw, i + 1), readu8(raw, i + 2)
				local o = out + x * 4
				writeu8(pixels, o, r)
				writeu8(pixels, o + 1, g)
				writeu8(pixels, o + 2, b)
				writeu8(pixels, o + 3, if r == keyRed and g == keyGreen and b == keyBlue then 0 else 255)
			end
		elseif colorType == 3 then
			local entries = palette :: buffer
			local perByte = 8 // depth
			local mask = 2 ^ depth - 1
			for x = 0, width - 1 do
				local index: number
				if depth == 8 then
					index = readu8(raw, row + x)
				else
					local byte = readu8(raw, row + x // perByte)
					local shift = 8 - depth * (x % perByte + 1)
					index = band(rshift(byte, shift), mask)
				end
				if index >= paletteCount then
					fail("palette index out of range")
				end
				buffer.writeu32(pixels, out + x * 4, buffer.readu32(entries, index * 4))
			end
		elseif colorType == 6 then
			for x = 0, width - 1 do
				local i = row + x * 8
				local o = out + x * 4
				writeu8(pixels, o, readu8(raw, i))
				writeu8(pixels, o + 1, readu8(raw, i + 2))
				writeu8(pixels, o + 2, readu8(raw, i + 4))
				writeu8(pixels, o + 3, readu8(raw, i + 6))
			end
		elseif colorType == 2 then
			for x = 0, width - 1 do
				local i = row + x * 6
				local o = out + x * 4
				local r, g, b = u16be(raw, i), u16be(raw, i + 2), u16be(raw, i + 4)
				writeu8(pixels, o, rshift(r, 8))
				writeu8(pixels, o + 1, rshift(g, 8))
				writeu8(pixels, o + 2, rshift(b, 8))
				writeu8(pixels, o + 3, if r == keyRed and g == keyGreen and b == keyBlue then 0 else 255)
			end
		elseif colorType == 4 then
			local step = if depth == 8 then 2 else 4
			local alphaAt = if depth == 8 then 1 else 2
			for x = 0, width - 1 do
				local i = row + x * step
				local o = out + x * 4
				local gray = readu8(raw, i)
				writeu8(pixels, o, gray)
				writeu8(pixels, o + 1, gray)
				writeu8(pixels, o + 2, gray)
				writeu8(pixels, o + 3, readu8(raw, i + alphaAt))
			end
		else
			for x = 0, width - 1 do
				local sample: number, gray: number
				if depth == 16 then
					sample = u16be(raw, row + x * 2)
					gray = rshift(sample, 8)
				elseif depth == 8 then
					sample = readu8(raw, row + x)
					gray = sample
				else
					local perByte = 8 // depth
					local byte = readu8(raw, row + x // perByte)
					local shift = 8 - depth * (x % perByte + 1)
					sample = band(rshift(byte, shift), 2 ^ depth - 1)
					gray = sample * 255 // (2 ^ depth - 1)
				end
				local o = out + x * 4
				writeu8(pixels, o, gray)
				writeu8(pixels, o + 1, gray)
				writeu8(pixels, o + 2, gray)
				writeu8(pixels, o + 3, if sample == keyGray then 0 else 255)
			end
		end
	end
	return { width = width, height = height, pixels = pixels }
end

return Png
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBXA2FA459B71FFFF6660510320A27D3771">
			<Properties>
				<string name="Name">Rij</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local Rij = {}

export type Spec = {
	kind: string,
	enum: string?,
	design: boolean,
	max: number?,
	studio: boolean?,
}

export type ImageRef = { image: string }

local function design(kind: string, enum: string?, max: number?): Spec
	return { kind = kind, enum = enum, design = true, max = max }
end

local function extra(kind: string, enum: string?, max: number?): Spec
	return { kind = kind, enum = enum, design = false, max = max }
end

local function studio(kind: string): Spec
	return { kind = kind, design = false, studio = true }
end

local TEXT_LIMIT = 200000

local GROUPS: { [string]: { [string]: Spec } } = {
	GuiBase2d = {
		AutoLocalize = extra("bool"),
		SelectionGroup = extra("bool"),
		SelectionBehaviorUp = extra("Enum", "SelectionBehavior"),
		SelectionBehaviorDown = extra("Enum", "SelectionBehavior"),
		SelectionBehaviorLeft = extra("Enum", "SelectionBehavior"),
		SelectionBehaviorRight = extra("Enum", "SelectionBehavior"),
	},
	GuiObject = {
		Active = extra("bool"),
		AnchorPoint = design("Vector2"),
		AutomaticSize = design("Enum", "AutomaticSize"),
		BackgroundColor3 = design("Color3"),
		BackgroundTransparency = design("number"),
		BorderColor3 = design("Color3"),
		BorderMode = design("Enum", "BorderMode"),
		BorderSizePixel = design("int"),
		ClipsDescendants = design("bool"),
		Interactable = extra("bool"),
		LayoutOrder = design("int"),
		Position = design("UDim2"),
		Rotation = design("number"),
		Selectable = extra("bool"),
		SelectionOrder = extra("int"),
		Size = design("UDim2"),
		SizeConstraint = extra("Enum", "SizeConstraint"),
		Visible = design("bool"),
		ZIndex = design("int"),
	},
	Frame = {
		Style = extra("Enum", "FrameStyle"),
	},
	GuiButton = {
		AutoButtonColor = extra("bool"),
		Modal = extra("bool"),
		Selected = extra("bool"),
		Style = extra("Enum", "ButtonStyle"),
	},
	Text = {
		Text = design("string", nil, TEXT_LIMIT),
		FontFace = design("Font"),
		Font = extra("Enum", "Font"),
		LineHeight = design("number"),
		MaxVisibleGraphemes = extra("int"),
		OpenTypeFeatures = extra("string", nil, 1000),
		RichText = design("bool"),
		TextColor3 = design("Color3"),
		TextDirection = extra("Enum", "TextDirection"),
		TextScaled = design("bool"),
		TextSize = design("number"),
		TextStrokeColor3 = design("Color3"),
		TextStrokeTransparency = design("number"),
		TextTransparency = design("number"),
		TextTruncate = design("Enum", "TextTruncate"),
		TextWrapped = design("bool"),
		TextXAlignment = design("Enum", "TextXAlignment"),
		TextYAlignment = design("Enum", "TextYAlignment"),
	},
	TextBox = {
		ClearTextOnFocus = extra("bool"),
		MultiLine = extra("bool"),
		PlaceholderColor3 = extra("Color3"),
		PlaceholderText = extra("string", nil, 10000),
		ShowNativeInput = extra("bool"),
		TextEditable = extra("bool"),
	},
	Image = {
		Image = design("Image"),
		ImageColor3 = design("Color3"),
		ImageRectOffset = design("Vector2"),
		ImageRectSize = design("Vector2"),
		ImageTransparency = design("number"),
		ResampleMode = design("Enum", "ResamplerMode"),
		ScaleType = design("Enum", "ScaleType"),
		SliceCenter = design("Rect"),
		SliceScale = design("number"),
		TileSize = design("UDim2"),
	},
	ImageButton = {
		HoverImage = extra("Image"),
		PressedImage = extra("Image"),
	},
	ScrollingFrame = {
		AutomaticCanvasSize = extra("Enum", "AutomaticSize"),
		BottomImage = extra("Image"),
		CanvasPosition = extra("Vector2"),
		CanvasSize = extra("UDim2"),
		ElasticBehavior = extra("Enum", "ElasticBehavior"),
		HorizontalScrollBarInset = extra("Enum", "ScrollBarInset"),
		MidImage = extra("Image"),
		ScrollBarImageColor3 = extra("Color3"),
		ScrollBarImageTransparency = extra("number"),
		ScrollBarThickness = extra("int"),
		ScrollingDirection = extra("Enum", "ScrollingDirection"),
		ScrollingEnabled = extra("bool"),
		TopImage = extra("Image"),
		VerticalScrollBarInset = extra("Enum", "ScrollBarInset"),
		VerticalScrollBarPosition = extra("Enum", "VerticalScrollBarPosition"),
	},
	CanvasGroup = {
		GroupColor3 = design("Color3"),
		GroupTransparency = design("number"),
	},
	VideoFrame = {
		Looped = studio("bool"),
		Playing = studio("bool"),
		TimePosition = studio("number"),
		Video = studio("Image"),
		Volume = studio("number"),
	},
	ViewportFrame = {
		Ambient = studio("Color3"),
		ImageColor3 = studio("Color3"),
		ImageTransparency = studio("number"),
		LightColor = studio("Color3"),
		LightDirection = studio("Vector3"),
	},
	LayerCollector = {
		Enabled = extra("bool"),
		ResetOnSpawn = design("bool"),
		ZIndexBehavior = design("Enum", "ZIndexBehavior"),
	},
	ScreenGui = {
		ClipToDeviceSafeArea = extra("bool"),
		DisplayOrder = design("int"),
		IgnoreGuiInset = design("bool"),
		SafeAreaCompatibility = extra("Enum", "SafeAreaCompatibility"),
		ScreenInsets = design("Enum", "ScreenInsets"),
	},
	SurfaceGui = {
		Active = extra("bool"),
		AlwaysOnTop = extra("bool"),
		Brightness = extra("number"),
		CanvasSize = extra("Vector2"),
		ClipsDescendants = extra("bool"),
		Face = extra("Enum", "NormalId"),
		LightInfluence = extra("number"),
		MaxDistance = extra("number"),
		PixelsPerStud = extra("number"),
		SizingMode = extra("Enum", "SurfaceGuiSizingMode"),
		ToolPunchThroughDistance = extra("number"),
		ZOffset = extra("number"),
	},
	BillboardGui = {
		Active = extra("bool"),
		AlwaysOnTop = extra("bool"),
		Brightness = extra("number"),
		ClipsDescendants = extra("bool"),
		DistanceStep = extra("number"),
		LightInfluence = extra("number"),
		MaxDistance = extra("number"),
		Size = extra("UDim2"),
		SizeOffset = extra("Vector2"),
	},
	UICorner = {
		CornerRadius = design("UDim"),
		TopLeftRadius = design("UDim"),
		TopRightRadius = design("UDim"),
		BottomRightRadius = design("UDim"),
		BottomLeftRadius = design("UDim"),
	},
	UIStroke = {
		ApplyStrokeMode = design("Enum", "ApplyStrokeMode"),
		BorderOffset = design("UDim"),
		BorderStrokePosition = design("Enum", "BorderStrokePosition"),
		Color = design("Color3"),
		Enabled = design("bool"),
		LineJoinMode = design("Enum", "LineJoinMode"),
		StrokeSizingMode = design("Enum", "StrokeSizingMode"),
		Thickness = design("number"),
		Transparency = design("number"),
		ZIndex = design("int"),
	},
	UIGradient = {
		Color = design("ColorSequence"),
		Enabled = design("bool"),
		Offset = design("Vector2"),
		Rotation = design("number"),
		Scale = design("number"),
		TileMode = design("Enum", "GradientTileMode"),
		Transparency = design("NumberSequence"),
		Type = design("Enum", "GradientType"),
	},
	UIGridStyleLayout = {
		FillDirection = design("Enum", "FillDirection"),
		HorizontalAlignment = design("Enum", "HorizontalAlignment"),
		SortOrder = design("Enum", "SortOrder"),
		VerticalAlignment = design("Enum", "VerticalAlignment"),
	},
	UIListLayout = {
		HorizontalFlex = design("Enum", "UIFlexAlignment"),
		ItemLineAlignment = design("Enum", "ItemLineAlignment"),
		Padding = design("UDim"),
		VerticalFlex = design("Enum", "UIFlexAlignment"),
		Wraps = design("bool"),
	},
	UIGridLayout = {
		CellPadding = design("UDim2"),
		CellSize = design("UDim2"),
		FillDirectionMaxCells = design("int"),
		StartCorner = design("Enum", "StartCorner"),
	},
	UIPageLayout = {
		Animated = design("bool"),
		Circular = design("bool"),
		EasingDirection = design("Enum", "EasingDirection"),
		EasingStyle = design("Enum", "EasingStyle"),
		GamepadInputEnabled = design("bool"),
		Padding = design("UDim"),
		ScrollWheelInputEnabled = design("bool"),
		TouchInputEnabled = design("bool"),
		TweenTime = design("number"),
	},
	UITableLayout = {
		FillEmptySpaceColumns = design("bool"),
		FillEmptySpaceRows = design("bool"),
		MajorAxis = design("Enum", "TableMajorAxis"),
		Padding = design("UDim2"),
	},
	UIPadding = {
		PaddingBottom = design("UDim"),
		PaddingLeft = design("UDim"),
		PaddingRight = design("UDim"),
		PaddingTop = design("UDim"),
	},
	UIFlexItem = {
		FlexMode = design("Enum", "UIFlexMode"),
		GrowRatio = design("number"),
		ItemLineAlignment = design("Enum", "ItemLineAlignment"),
		ShrinkRatio = design("number"),
	},
	UIAspectRatioConstraint = {
		AspectRatio = design("number"),
		AspectType = design("Enum", "AspectType"),
		DominantAxis = design("Enum", "DominantAxis"),
	},
	UISizeConstraint = {
		MaxSize = design("Vector2"),
		MinSize = design("Vector2"),
	},
	UITextSizeConstraint = {
		MaxTextSize = design("int"),
		MinTextSize = design("int"),
	},
	UIScale = {
		Scale = design("number"),
	},
	UIShadow = {
		BlurRadius = design("UDim"),
		Color = design("Color3"),
		Enabled = design("bool"),
		Offset = design("UDim2"),
		Spread = design("UDim2"),
		Transparency = design("number"),
		ZIndex = design("int"),
		Inset = extra("bool"),
		ShowBehindParent = extra("bool"),
	},
}

local CLASS_GROUPS: { [string]: { string } } = {
	ScreenGui = { "GuiBase2d", "LayerCollector", "ScreenGui" },
	SurfaceGui = { "GuiBase2d", "LayerCollector", "SurfaceGui" },
	BillboardGui = { "GuiBase2d", "LayerCollector", "BillboardGui" },
	Frame = { "GuiBase2d", "GuiObject", "Frame" },
	ScrollingFrame = { "GuiBase2d", "GuiObject", "ScrollingFrame" },
	CanvasGroup = { "GuiBase2d", "GuiObject", "CanvasGroup" },
	TextLabel = { "GuiBase2d", "GuiObject", "Text" },
	TextButton = { "GuiBase2d", "GuiObject", "GuiButton", "Text" },
	TextBox = { "GuiBase2d", "GuiObject", "Text", "TextBox" },
	ImageLabel = { "GuiBase2d", "GuiObject", "Image" },
	ImageButton = { "GuiBase2d", "GuiObject", "GuiButton", "Image", "ImageButton" },
	VideoFrame = { "GuiBase2d", "GuiObject", "VideoFrame" },
	ViewportFrame = { "GuiBase2d", "GuiObject", "ViewportFrame" },
	UICorner = { "UICorner" },
	UIStroke = { "UIStroke" },
	UIGradient = { "UIGradient" },
	UIListLayout = { "UIGridStyleLayout", "UIListLayout" },
	UIGridLayout = { "UIGridStyleLayout", "UIGridLayout" },
	UIPageLayout = { "UIGridStyleLayout", "UIPageLayout" },
	UITableLayout = { "UIGridStyleLayout", "UITableLayout" },
	UIPadding = { "UIPadding" },
	UIFlexItem = { "UIFlexItem" },
	UIAspectRatioConstraint = { "UIAspectRatioConstraint" },
	UISizeConstraint = { "UISizeConstraint" },
	UITextSizeConstraint = { "UITextSizeConstraint" },
	UIScale = { "UIScale" },
	UIShadow = { "UIShadow" },
	Folder = {},
}

local SET_FIRST = { CornerRadius = 1, Font = 2, TextWrapped = 3 }
local SET_LAST = { ScreenInsets = 1 }

local function setOrder(a: string, b: string): boolean
	local firstA, firstB = SET_FIRST[a], SET_FIRST[b]
	if firstA or firstB then
		if firstA and firstB then
			return firstA < firstB
		end
		return firstA ~= nil
	end
	local lastA, lastB = SET_LAST[a], SET_LAST[b]
	if lastA or lastB then
		if lastA and lastB then
			return lastA < lastB
		end
		return lastB ~= nil
	end
	return a < b
end

local SPECS: { [string]: { [string]: Spec } } = {}
local NAMES: { [string]: { string } } = {}
for className, groups in CLASS_GROUPS do
	local specs: { [string]: Spec } = {}
	local names: { string } = {}
	for _, group in groups do
		for property, spec in GROUPS[group] do
			specs[property] = spec
		end
	end
	for property in specs do
		table.insert(names, property)
	end
	table.sort(names, setOrder)
	SPECS[className] = table.freeze(specs)
	NAMES[className] = table.freeze(names)
end

local LAYER_COLLECTORS = { ScreenGui = true, SurfaceGui = true, BillboardGui = true }
local HELPERS = {
	UICorner = true,
	UIStroke = true,
	UIGradient = true,
	UIListLayout = true,
	UIGridLayout = true,
	UIPageLayout = true,
	UITableLayout = true,
	UIPadding = true,
	UIFlexItem = true,
	UIAspectRatioConstraint = true,
	UISizeConstraint = true,
	UITextSizeConstraint = true,
	UIScale = true,
	UIShadow = true,
}

local ATTRIBUTE_KINDS = {
	bool = true,
	number = true,
	int = true,
	string = true,
	UDim = true,
	UDim2 = true,
	Vector2 = true,
	Color3 = true,
	NumberRange = true,
	NumberSequence = true,
	ColorSequence = true,
	Rect = true,
	Font = true,
	Enum = true,
}

local ACCEPTS: { [string]: { [string]: boolean } } = {
	number = { number = true, int = true },
	int = { int = true, number = true },
	Image = { Image = true, Content = true, string = true },
}

local HUGE = 3.4e38

local function endless(value: number): number
	if value >= HUGE then
		return math.huge
	elseif value <= -HUGE then
		return -math.huge
	end
	return value
end

local function isNumber(value: any): boolean
	return type(value) == "number" and value == value and value ~= math.huge and value ~= -math.huge
end

local function isInteger(value: any): boolean
	return isNumber(value) and value == math.floor(value) and value >= -2147483648 and value <= 2147483647
end

function Rij.isContent(value: any): boolean
	if type(value) ~= "string" then
		return false
	end
	if value == "" then
		return true
	end
	if string.match(value, "^rbxassetid://%d+$") then
		return true
	end
	if string.match(value, "^rbxasset://[%w_%-%./@]+$") and not string.find(value, "..", 1, true) then
		return true
	end
	if string.match(value, "^rbxthumb://[%w_%-=&%.]+$") then
		return true
	end
	if string.match(value, "^https?://www%.roblox%.com/asset/?%?id=%d+$") then
		return true
	end
	return false
end

local function isFontFamily(value: any): boolean
	return type(value) == "string"
		and (
			string.match(value, "^rbxasset://fonts/families/[%w_%-]+%.json$") ~= nil
			or string.match(value, "^rbxassetid://%d+$") ~= nil
		)
end

local function enumItem(enumType: any, item: any): EnumItem?
	if type(enumType) ~= "string" or (type(item) ~= "string" and type(item) ~= "number") then
		return nil
	end
	local ok, enum = pcall(function()
		return (Enum :: any)[enumType]
	end)
	if not ok or enum == nil then
		return nil
	end
	local found
	if type(item) == "string" then
		found = enum:FromName(item)
	else
		found = enum:FromValue(item)
	end
	return found
end

local function color(hex: any): Color3?
	if type(hex) ~= "string" or not string.match(hex, "^#?%x%x%x%x%x%x$") then
		return nil
	end
	return Color3.fromHex(hex)
end

local function keypointTimes(points: { any }): boolean
	if #points < 2 or #points > 20 then
		return false
	end
	local last = -1
	for index, point in points do
		local t = point[1]
		if not isNumber(t) or t < last or t < 0 or t > 1 then
			return false
		end
		if (index == 1 and t ~= 0) or (index == #points and t ~= 1) then
			return false
		end
		last = t
	end
	return true
end

local function keypoints(raw: { any }): { any }?
	local points = {}
	for index = 2, #raw do
		local point = raw[index]
		if type(point) ~= "table" then
			return nil
		end
		table.insert(points, point)
	end
	return points
end

function Rij.decode(raw: any): (boolean, any)
	if type(raw) ~= "table" or type(raw[1]) ~= "string" then
		return false, "is not a value"
	end
	local kind = raw[1]
	local a, b, c, d = raw[2], raw[3], raw[4], raw[5]
	if kind == "bool" then
		if type(a) == "boolean" then
			return true, a
		end
	elseif kind == "number" then
		if isNumber(a) then
			return true, endless(a)
		end
	elseif kind == "int" then
		if isInteger(a) then
			return true, a
		end
	elseif kind == "string" then
		if type(a) == "string" then
			return true, a
		end
	elseif kind == "UDim" then
		if isNumber(a) and isNumber(b) then
			return true, UDim.new(a, b)
		end
	elseif kind == "UDim2" then
		if isNumber(a) and isNumber(b) and isNumber(c) and isNumber(d) then
			return true, UDim2.new(a, b, c, d)
		end
	elseif kind == "Vector2" then
		if isNumber(a) and isNumber(b) then
			return true, Vector2.new(endless(a), endless(b))
		end
	elseif kind == "Vector3" then
		if isNumber(a) and isNumber(b) and isNumber(c) then
			return true, Vector3.new(a, b, c)
		end
	elseif kind == "Color3" then
		local value = color(a)
		if value then
			return true, value
		end
	elseif kind == "Enum" then
		local item = enumItem(a, b)
		if item then
			return true, item
		end
		return false, "is not a known Enum." .. tostring(a) .. " item"
	elseif kind == "Font" then
		local weight = if b == nil then Enum.FontWeight.Regular else enumItem("FontWeight", b)
		local style = if c == nil then Enum.FontStyle.Normal else enumItem("FontStyle", c)
		if isFontFamily(a) and weight and style then
			return true, Font.new(a, weight :: any, style :: any)
		end
	elseif kind == "ColorSequence" then
		local points = keypoints(raw)
		if points and keypointTimes(points) then
			local list = {}
			for _, point in points do
				local value = color(point[2])
				if not value then
					return false, "has a bad color"
				end
				table.insert(list, ColorSequenceKeypoint.new(point[1], value))
			end
			local ok, value = pcall(function()
				return ColorSequence.new(list)
			end)
			if ok then
				return true, value
			end
		end
	elseif kind == "NumberSequence" then
		local points = keypoints(raw)
		if points and keypointTimes(points) then
			local list = {}
			for _, point in points do
				local value, envelope = point[2], point[3] or 0
				if not isNumber(value) or not isNumber(envelope) then
					return false, "has a bad number"
				end
				table.insert(list, NumberSequenceKeypoint.new(point[1], value, envelope))
			end
			local ok, value = pcall(function()
				return NumberSequence.new(list)
			end)
			if ok then
				return true, value
			end
		end
	elseif kind == "NumberRange" then
		if isNumber(a) and isNumber(b) and a <= b then
			return true, NumberRange.new(endless(a), endless(b))
		end
	elseif kind == "Rect" then
		if isNumber(a) and isNumber(b) and isNumber(c) and isNumber(d) then
			return true, Rect.new(a, b, c, d)
		end
	elseif kind == "Content" then
		if Rij.isContent(a) then
			return true, a
		end
		return false, "is not a Roblox image address"
	elseif kind == "Image" then
		if type(a) == "string" and #a == 64 and string.match(a, "^%x+$") then
			local ref: ImageRef = { image = string.lower(a) }
			return true, ref
		end
		return false, "is not an image of the file"
	else
		return false, "has an unknown type (" .. kind .. ")"
	end
	return false, "is not a valid " .. kind
end

function Rij.isImageRef(value: any): boolean
	return type(value) == "table" and type(value.image) == "string"
end

function Rij.decodeProperty(className: string, property: string, raw: any): (boolean, any)
	local specs = SPECS[className]
	local spec = specs and specs[property]
	if spec == nil then
		return false, "isn't a property the plugin sets on a " .. className
	end
	if type(raw) ~= "table" or type(raw[1]) ~= "string" then
		return false, "is not a value"
	end
	local kind = raw[1]
	local accepted = ACCEPTS[spec.kind]
	if (accepted and not accepted[kind]) or (accepted == nil and kind ~= spec.kind) then
		return false, "should be a " .. spec.kind .. ", not a " .. kind
	end
	if spec.kind == "Enum" and raw[2] ~= spec.enum then
		return false, "should be an Enum." .. tostring(spec.enum)
	end
	local ok, value = Rij.decode(raw)
	if not ok then
		return false, value
	end
	if spec.kind == "int" and not isInteger(value) then
		return false, "should be a whole number"
	end
	if spec.kind == "string" and #value > (spec.max or TEXT_LIMIT) then
		return false, "is too long"
	end
	if spec.kind == "Image" and kind == "string" and not Rij.isContent(value) then
		return false, "is not a Roblox image address"
	end
	return true, value
end

function Rij.decodeAttribute(name: string, raw: any): (boolean, any)
	if type(name) ~= "string" or #name == 0 or #name > 100 or not string.match(name, "^[%w_]+$") then
		return false, "has a name Roblox doesn't allow"
	end
	if string.sub(name, 1, 3) == "RBX" then
		return false, "has a name Roblox keeps for itself"
	end
	if type(raw) ~= "table" or not ATTRIBUTE_KINDS[raw[1]] then
		return false, "has a type attributes can't hold"
	end
	return Rij.decode(raw)
end

function Rij.isTag(value: any): boolean
	return type(value) == "string" and #value > 0 and #value <= 100 and not string.find(value, "%c")
end

function Rij.isAllowedClass(className: any): boolean
	return type(className) == "string" and CLASS_GROUPS[className] ~= nil
end

function Rij.isHelper(className: string): boolean
	return HELPERS[className] == true
end

function Rij.isLayerCollector(className: string): boolean
	return LAYER_COLLECTORS[className] == true
end

function Rij.spec(className: string, property: string): Spec?
	local specs = SPECS[className]
	return specs and specs[property]
end

function Rij.designProperties(className: string): { string }
	local out = {}
	local names = NAMES[className]
	if names == nil then
		return out
	end
	for _, property in names do
		if SPECS[className][property].design then
			table.insert(out, property)
		end
	end
	return out
end

function Rij.isStudioSide(className: string, property: string): boolean
	local spec = Rij.spec(className, property)
	return spec ~= nil and spec.studio == true
end

function Rij.properties(className: string): { string }
	return NAMES[className] or {}
end

function Rij.classes(): { string }
	local out = {}
	for className in CLASS_GROUPS do
		table.insert(out, className)
	end
	table.sort(out)
	return out
end

return Rij
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBXB402D108772D0C7182B9C559D57083EF">
			<Properties>
				<string name="Name">Settings</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local Settings = {}
Settings.__index = Settings

export type Storage = {
	get: (key: string) -> any,
	set: (key: string, value: any) -> (),
}

export type LastImport = {
	project: string,
	at: number,
	added: number,
	updated: number,
	removed: number,
	uploaded: number,
	empty: number,
}

export type Release = {
	version: string,
	release: number,
	notes: string?,
	url: string,
}

export type UpdateState = {
	checkedAt: number?,
	newest: Release?,
	later: number?,
	off: boolean,
}

local KEY_TARGET = "NKD_Target"
local KEY_CREATOR = "NKD_Creator"
local KEY_IMAGES = "NKD_UploadedImages"
local KEY_LAST = "NKD_LastImport"
local KEY_VERSION = "NKD_SettingsVersion"
local KEY_UPDATES = "NKD_Updates"

Settings.TARGETS = { "file", "StarterGui", "ReplicatedStorage", "ServerStorage" }

Settings.VERSION = 1

local MIGRATIONS: { [number]: (storage: Storage) -> () } = {
	[1] = function(_storage: Storage) end,
}

export type Settings = typeof(setmetatable({} :: { storage: Storage }, Settings))

function Settings.new(storage: Storage): Settings
	return setmetatable({ storage = storage }, Settings)
end

function Settings.migrate(self: Settings): number
	local stored = self.storage.get(KEY_VERSION)
	local from = if type(stored) == "number" and stored == stored and stored >= 0 then math.floor(stored) else 0
	if from >= Settings.VERSION then
		return from
	end
	for version = from + 1, Settings.VERSION do
		local step = MIGRATIONS[version]
		if step then
			step(self.storage)
		end
		self.storage.set(KEY_VERSION, version)
	end
	return Settings.VERSION
end

function Settings.pluginStorage(plugin: Plugin): Storage
	return {
		get = function(key: string): any
			local ok, value = pcall(plugin.GetSetting, plugin, key)
			return if ok then value else nil
		end,
		set = function(key: string, value: any)
			pcall(plugin.SetSetting, plugin, key, value)
		end,
	}
end

function Settings.memoryStorage(): Storage
	local values: { [string]: any } = {}
	return {
		get = function(key: string): any
			return values[key]
		end,
		set = function(key: string, value: any)
			values[key] = value
		end,
	}
end

function Settings.target(self: Settings): string
	local value = self.storage.get(KEY_TARGET)
	return if type(value) == "string" and table.find(Settings.TARGETS, value) then value else "file"
end

function Settings.setTarget(self: Settings, value: string)
	if table.find(Settings.TARGETS, value) then
		self.storage.set(KEY_TARGET, value)
	end
end

function Settings.creator(self: Settings): { kind: string, groupId: number? }
	local value = self.storage.get(KEY_CREATOR)
	local groupId = nil
	if type(value) == "table" and type(value.groupId) == "number" and value.groupId > 0 and value.groupId == math.floor(value.groupId) then
		groupId = value.groupId
	end
	if type(value) == "table" and value.kind == "group" then
		return { kind = "group", groupId = groupId }
	end
	return { kind = "user", groupId = groupId }
end

function Settings.setCreator(self: Settings, kind: string, groupId: number?)
	self.storage.set(KEY_CREATOR, { kind = if kind == "group" then "group" else "user", groupId = groupId })
end

local function readImages(self: Settings): { [string]: number }
	local value = self.storage.get(KEY_IMAGES)
	local out: { [string]: number } = {}
	if type(value) == "table" then
		for hash, id in value do
			if type(hash) == "string" and type(id) == "number" and id > 0 then
				out[hash] = id
			end
		end
	end
	return out
end

function Settings.imageCache(self: Settings)
	local settings = self
	return {
		get = function(_cache: any, hash: string): number?
			return readImages(settings)[hash]
		end,
		set = function(_cache: any, hash: string, id: number)
			local images = readImages(settings)
			images[hash] = id
			settings.storage.set(KEY_IMAGES, images)
		end,
	}
end

function Settings.uploadedCount(self: Settings): number
	local count = 0
	for _ in readImages(self) do
		count += 1
	end
	return count
end

function Settings.forgetUploads(self: Settings)
	self.storage.set(KEY_IMAGES, {})
end

function Settings.lastImport(self: Settings): LastImport?
	local value = self.storage.get(KEY_LAST)
	if type(value) == "table" and type(value.project) == "string" and type(value.at) == "number" then
		return {
			project = value.project,
			at = value.at,
			added = tonumber(value.added) or 0,
			updated = tonumber(value.updated) or 0,
			removed = tonumber(value.removed) or 0,
			uploaded = tonumber(value.uploaded) or 0,
			empty = tonumber(value.empty) or 0,
		}
	end
	return nil
end

function Settings.setLastImport(self: Settings, value: LastImport)
	self.storage.set(KEY_LAST, value)
end

local function count(value: any): number?
	if type(value) == "number" and value == value and value >= 1 and value <= 1e6 and math.floor(value) == value then
		return value
	end
	return nil
end

function Settings.updates(self: Settings): UpdateState
	local value = self.storage.get(KEY_UPDATES)
	local state: UpdateState = { off = false }
	if type(value) ~= "table" then
		return state
	end
	local at = value.checkedAt
	if type(at) == "number" and at == at and at >= 0 and at < math.huge then
		state.checkedAt = at
	end
	state.later = count(value.later)
	state.off = value.off == true
	local newest = value.newest
	if type(newest) == "table" and type(newest.version) == "string" and count(newest.release) and type(newest.url) == "string" then
		state.newest = {
			version = newest.version,
			release = newest.release,
			notes = if type(newest.notes) == "string" then newest.notes else nil,
			url = newest.url,
		}
	end
	return state
end

function Settings.setUpdates(self: Settings, state: UpdateState)
	local newest = state.newest
	self.storage.set(KEY_UPDATES, {
		checkedAt = state.checkedAt,
		newest = if newest then { version = newest.version, release = newest.release, notes = newest.notes, url = newest.url } else nil,
		later = state.later,
		off = state.off,
	})
end

return Settings
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX85FC46B84397868B818BD998BBEB9BD6">
			<Properties>
				<string name="Name">Sha256</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict
--!native

local band, bnot, bxor, rrotate, rshift = bit32.band, bit32.bnot, bit32.bxor, bit32.rrotate, bit32.rshift
local byteswap = bit32.byteswap
local readu32 = buffer.readu32

local K = {
	0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
	0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
	0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
	0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
	0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
	0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
	0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
	0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
}

local TWO32 = 4294967296

local function compress(h: { number }, w: { number }, data: buffer, offset: number)
	for i = 1, 16 do
		w[i] = byteswap(readu32(data, offset + (i - 1) * 4))
	end
	for i = 17, 64 do
		local x, y = w[i - 15], w[i - 2]
		local s0 = bxor(rrotate(x, 7), rrotate(x, 18), rshift(x, 3))
		local s1 = bxor(rrotate(y, 17), rrotate(y, 19), rshift(y, 10))
		w[i] = (w[i - 16] + s0 + w[i - 7] + s1) % TWO32
	end
	local a, b, c, d, e, f, g, hh = h[1], h[2], h[3], h[4], h[5], h[6], h[7], h[8]
	for i = 1, 64 do
		local s1 = bxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25))
		local ch = bxor(band(e, f), band(bnot(e), g))
		local temp1 = hh + s1 + ch + K[i] + w[i]
		local s0 = bxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22))
		local maj = bxor(band(a, b), band(a, c), band(b, c))
		hh = g
		g = f
		f = e
		e = (d + temp1) % TWO32
		d = c
		c = b
		b = a
		a = (temp1 + s0 + maj) % TWO32
	end
	h[1] = (h[1] + a) % TWO32
	h[2] = (h[2] + b) % TWO32
	h[3] = (h[3] + c) % TWO32
	h[4] = (h[4] + d) % TWO32
	h[5] = (h[5] + e) % TWO32
	h[6] = (h[6] + f) % TWO32
	h[7] = (h[7] + g) % TWO32
	h[8] = (h[8] + hh) % TWO32
end

local Sha256 = {}

function Sha256.hex(data: buffer, offset: number?, length: number?): string
	local start = offset or 0
	local size = length or (buffer.len(data) - start)
	local h = { 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 }
	local w = table.create(64, 0)
	local blocks = size // 64
	for index = 0, blocks - 1 do
		compress(h, w, data, start + index * 64)
	end
	local rest = size - blocks * 64
	local tail = buffer.create(if rest < 56 then 64 else 128)
	buffer.copy(tail, 0, data, start + blocks * 64, rest)
	buffer.writeu8(tail, rest, 0x80)
	local bitLength = size * 8
	local tailSize = buffer.len(tail)
	buffer.writeu32(tail, tailSize - 8, byteswap(bitLength // TWO32))
	buffer.writeu32(tail, tailSize - 4, byteswap(bitLength % TWO32))
	compress(h, w, tail, 0)
	if tailSize == 128 then
		compress(h, w, tail, 64)
	end
	return string.format("%08x%08x%08x%08x%08x%08x%08x%08x", h[1], h[2], h[3], h[4], h[5], h[6], h[7], h[8])
end

return Sha256
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBXDF32FC2B985BE6009B93A23572EE71BF">
			<Properties>
				<string name="Name">Ui</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local PALETTE = {
	pink = Color3.fromHex("#ff5ca8"),
	button = {
		Color3.fromHex("#e3368b"),
		Color3.fromHex("#d61f78"),
		Color3.fromHex("#b3135f"),
	},
	buttonText = Color3.fromHex("#ffffff"),
	glass = {
		Color3.fromHex("#241f32"),
		Color3.fromHex("#17141f"),
	},
	violet = Color3.fromHex("#b98cff"),
	cyan = Color3.fromHex("#5ce1f5"),

	site = {
		background = Color3.fromHex("#08070b"),
		surface = Color3.fromHex("#110f17"),
		raised = Color3.fromHex("#1a1623"),
		line = Color3.fromHex("#241f30"),
		lineStrong = Color3.fromHex("#342c47"),
		strong = Color3.fromHex("#f5f3f9"),
		text = Color3.fromHex("#bdb7cc"),
		dim = Color3.fromHex("#8d879f"),
		warn = Color3.fromHex("#f6c453"),
		error = Color3.fromHex("#ff6b81"),
	},
}

type Glow = { color: Color3, transparency: number, blur: number, spread: number }
type Light = { name: string, at: UDim2, size: Vector2, drift: Vector2, seconds: number, glows: { Glow } }
local BACKDROP: { lights: { Light }, grid: number, cell: number } = {
	lights = {
		{
			name = "LightCyan",
			at = UDim2.fromOffset(40, 30),
			size = Vector2.new(150, 110),
			drift = Vector2.new(36, 20),
			seconds = 18,
			glows = { { color = PALETTE.cyan, transparency = 0.63, blur = 120, spread = 0 } },
		},
		{
			name = "LightPink",
			at = UDim2.new(1, -50, 0, 50),
			size = Vector2.new(170, 120),
			drift = Vector2.new(-44, 28),
			seconds = 22,
			glows = {
				{ color = PALETTE.violet, transparency = 0.87, blur = 160, spread = 80 },
				{ color = PALETTE.pink, transparency = 0.7, blur = 110, spread = 0 },
			},
		},
	},
	grid = 0.045,
	cell = 64,
}

local GuiService = game:GetService("GuiService")
local TextService = game:GetService("TextService")
local TweenService = game:GetService("TweenService")

local Brand = require(script.Parent.Brand)

local Ui = {}
Ui.__index = Ui

local CODE_START: { string } = {}
for _, prefix in { Brand.pastePrefix, table.unpack(Brand.legacyPastePrefixes) } do
	local letters = string.match(prefix, "^(%a+)") or prefix
	if not table.find(CODE_START, letters) then
		table.insert(CODE_START, letters)
	end
end

local function hasCode(text: string): boolean
	for _, letters in CODE_START do
		if string.find(text, letters, 1, true) then
			return true
		end
	end
	return false
end

export type Handlers = {
	paste: (text: string) -> (),
	importFile: () -> (),
	export: () -> (),
	confirm: () -> (),
	cancel: () -> (),
	placeholders: () -> (),
	dismiss: () -> (),
	setTarget: (value: string) -> (),
	setCreator: (kind: string, groupId: number?) -> (),
	forgetUploads: () -> (),
	hideNotice: (id: string) -> (),
	noticeAction: ((id: string, action: string) -> ())?,
	setUpdateCheck: ((on: boolean) -> ())?,
}

export type Action = { id: string, label: string, primary: boolean? }

export type State = {
	view: string,
	title: string,
	text: string?,
	lines: { string }?,
	warnings: { string }?,
	notes: { string }?,
	steps: { string }?,
	progress: number?,
	segments: { boolean }?,
	tone: string?,
	undo: boolean?,
	link: string?,
	actions: { Action }?,
	busy: boolean?,
}

export type SettingsState = {
	target: string,
	targetChoices: { { value: string, label: string } },
	creatorKind: string,
	groupId: number?,
	userName: string?,
	uploaded: number,
	updateCheck: boolean?,
}

export type Notice = {
	id: string,
	title: string,
	text: string,
	link: string?,
	tone: string?,
	steps: { string }?,
	actions: { Action }?,
}

export type Info = {
	pluginName: string,
	toolName: string,
	guideUrl: string,
	version: string?,
	copyright: string?,
	theme: StudioTheme?,
	reducedMotion: boolean?,
}

type Colors = {
	dark: boolean,
	background: Color3,
	surface: Color3,
	raised: Color3,
	line: Color3,
	lineStrong: Color3,
	strong: Color3,
	text: Color3,
	dim: Color3,
	accent: Color3,
	warn: Color3,
	error: Color3,
}

local FONT = "rbxasset://fonts/families/BuilderSans.json"
local MAX_WIDTH = 460
local HEADER_HEIGHT = 56
local FOOTER_HEIGHT = 26
local PAD = 12
local COLUMN_TOP = 12
local COLUMN_BOTTOM = 20
local LOGO = 32
local ICON_SIZE = 14
local ICON_GAP = 8

local WHITE = Color3.new(1, 1, 1)
local BLACK = Color3.new(0, 0, 0)

local QUICK = TweenInfo.new(0.12, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local EASE = TweenInfo.new(0.28, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)
local GLOW = TweenInfo.new(0.42, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)
local SPRING = TweenInfo.new(0.32, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
local PRESS = TweenInfo.new(0.09, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local NUDGE = TweenInfo.new(0.42, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
local SWEEP = TweenInfo.new(0.85, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)
local POP = TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
local PULSE = TweenInfo.new(0.4, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local GLIDE = TweenInfo.new(0.4, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)
local INSTANT = TweenInfo.new(0)
local REVEAL = 0.32
local STAGGER = 0.05
local SLIDE = 8
local RISE = 10
local SINK = 6
local LEAVE = 0.16
local WINDOW_FADE = 0.4
local SPIN = 0.9
local LIFT = 2
local PRESSED = 0.965

local GLOW_REST: { [string]: any } = { Offset = UDim2.fromOffset(0, 10), BlurRadius = UDim.new(0, 28), Transparency = 0.3 }
local GLOW_HOVER: { [string]: any } = { Offset = UDim2.fromOffset(0, 16), BlurRadius = UDim.new(0, 40), Transparency = 0.1 }

local PAGE_PARTS: { [string]: string } = { main = "Main", settings = "Settings", help = "Help" }

local function readColors(): Colors
	local site = PALETTE.site
	return {
		dark = true,
		background = site.background,
		surface = site.surface,
		raised = site.raised,
		line = site.line,
		lineStrong = site.lineStrong,
		strong = site.strong,
		text = site.text,
		dim = site.dim,
		accent = PALETTE.pink,
		warn = site.warn,
		error = site.error,
	}
end

type FadeEntry = { value: number, owner: {} }
type FadeTarget = { object: Instance, name: string, built: number }
type FadeOptions = {
	delay: number?,
	slide: boolean?,
	time: number?,
	since: number?,
	shallow: boolean?,
}

export type Ui = typeof(setmetatable(
	{} :: {
		root: Frame,
		handlers: Handlers,
		info: Info,
		colors: Colors,
		state: State,
		settingsState: SettingsState?,
		notices: { Notice },
		page: string,
		actions: { [GuiObject]: () -> () },
		looks: { [GuiObject]: (state: string) -> () },
		connections: { RBXScriptConnection },
		parts: { [string]: GuiObject },
		loops: { [string]: Tween },
		resting: { [Instance]: { [string]: FadeEntry } },
		leaving: { [GuiObject]: {} },
		progressShown: number,
		spinAngle: number,
		segmentsShown: { boolean },
		noticeIds: { string },
		revealedAt: number,
		still: boolean,
		awake: boolean,
		pop: boolean,
		pasteLook: (() -> ())?,
		destroyed: boolean,
	},
	Ui
))

local function font(weight: Enum.FontWeight): Font
	return Font.new(FONT, weight, Enum.FontStyle.Normal)
end

local function textWidth(text: string, size: number): number
	local ok, bounds = pcall(function()
		return TextService:GetTextSize(text, size, Enum.Font.BuilderSansBold, Vector2.new(10000, 1000))
	end)
	return if ok then math.ceil(bounds.X) else math.ceil(#text * size * 0.6)
end

local function corner(parent: Instance, radius: number)
	local item = Instance.new("UICorner")
	item.CornerRadius = if radius < 0 then UDim.new(0.5, 0) else UDim.new(0, radius)
	item.Parent = parent
end

local function stroke(parent: Instance, color: Color3, thickness: number?, transparency: number?): UIStroke
	local item = Instance.new("UIStroke")
	item.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
	item.Color = color
	item.Thickness = thickness or 1
	item.Transparency = transparency or 0
	item.Parent = parent
	return item
end

local function inside(edge: UIStroke): UIStroke
	pcall(function()
		edge.BorderStrokePosition = Enum.BorderStrokePosition.Inner
	end)
	return edge
end

local SHADOW_CLASS: string = "UIShadow"
local function softShadow(parent: Instance, color: Color3, transparency: number, blur: number, offsetY: number?, spread: number?, zIndex: number?): any
	local made, created = pcall(Instance.new, SHADOW_CLASS)
	if not made then
		return nil
	end
	local shadow: any = created
	local ok = pcall(function()
		shadow.Color = color
		shadow.Transparency = transparency
		shadow.BlurRadius = UDim.new(0, blur)
		shadow.Offset = UDim2.fromOffset(0, offsetY or 0)
	end)
	if not ok then
		return nil
	end
	if spread and spread ~= 0 then
		pcall(function()
			shadow.Spread = UDim2.fromOffset(spread, spread)
		end)
	end
	if zIndex then
		pcall(function()
			shadow.ZIndex = zIndex
		end)
	end
	shadow.Parent = parent
	return shadow
end

local function litFromAbove(edge: UIStroke, top: number, bottom: number)
	local light = Instance.new("UIGradient")
	light.Rotation = 90
	light.Transparency = NumberSequence.new({
		NumberSequenceKeypoint.new(0, top),
		NumberSequenceKeypoint.new(0.4, (top + bottom) / 2),
		NumberSequenceKeypoint.new(1, bottom),
	})
	light.Parent = edge
end

local function padding(parent: Instance, top: number, right: number, bottom: number, left: number)
	local item = Instance.new("UIPadding")
	item.PaddingTop = UDim.new(0, top)
	item.PaddingRight = UDim.new(0, right)
	item.PaddingBottom = UDim.new(0, bottom)
	item.PaddingLeft = UDim.new(0, left)
	item.Parent = parent
end

local function list(parent: Instance, gap: number, horizontal: boolean?): UIListLayout
	local item = Instance.new("UIListLayout")
	item.SortOrder = Enum.SortOrder.LayoutOrder
	item.Padding = UDim.new(0, gap)
	item.FillDirection = if horizontal then Enum.FillDirection.Horizontal else Enum.FillDirection.Vertical
	item.Parent = parent
	return item
end

local function blank(name: string, order: number?): Frame
	local frame = Instance.new("Frame")
	frame.Name = name
	frame.BackgroundTransparency = 1
	frame.BorderSizePixel = 0
	frame.LayoutOrder = order or 0
	return frame
end

local function stack(parent: Instance, name: string, order: number, gap: number): Frame
	local frame = blank(name, order)
	frame.Size = UDim2.new(1, 0, 0, 0)
	frame.AutomaticSize = Enum.AutomaticSize.Y
	list(frame, gap)
	frame.Parent = parent
	return frame
end

local function flow(parent: Instance, name: string, order: number, gap: number): Frame
	local frame = blank(name, order)
	frame.Size = UDim2.new(1, 0, 0, 0)
	frame.AutomaticSize = Enum.AutomaticSize.Y
	local layout = list(frame, gap, true)
	layout.Wraps = true
	layout.HorizontalFlex = Enum.UIFlexAlignment.Fill
	layout.VerticalAlignment = Enum.VerticalAlignment.Center
	frame.Parent = parent
	return frame
end

local function label(parent: Instance, name: string, text: string, size: number, color: Color3, weight: Enum.FontWeight, order: number): TextLabel
	local item = Instance.new("TextLabel")
	item.Name = name
	item.BackgroundTransparency = 1
	item.BorderSizePixel = 0
	item.Size = UDim2.new(1, 0, 0, 0)
	item.AutomaticSize = Enum.AutomaticSize.Y
	item.TextWrapped = true
	item.TextXAlignment = Enum.TextXAlignment.Left
	item.TextYAlignment = Enum.TextYAlignment.Top
	item.FontFace = font(weight)
	item.TextSize = size
	item.LineHeight = if size <= 14 then 1.12 else 1.05
	item.TextColor3 = color
	item.Text = text
	item.LayoutOrder = order
	item.Parent = parent
	return item
end

local function badge(parent: Instance, name: string, text: string, color: Color3, size: number, order: number): (Frame, Frame)
	local holder = blank(name, order)
	holder.Size = UDim2.fromOffset(size, size)
	local circle = Instance.new("Frame")
	circle.Name = "Circle"
	circle.BackgroundColor3 = color
	circle.BackgroundTransparency = 0.86
	circle.BorderSizePixel = 0
	circle.Size = UDim2.fromScale(1, 1)
	corner(circle, -1)
	local glyph = Instance.new("TextLabel")
	glyph.Name = "Glyph"
	glyph.BackgroundTransparency = 1
	glyph.Size = UDim2.fromScale(1, 1)
	glyph.FontFace = font(Enum.FontWeight.Bold)
	glyph.TextSize = math.floor(size * 0.6 + 0.5)
	glyph.TextColor3 = color
	glyph.Text = text
	glyph.Parent = circle
	circle.Parent = holder
	holder.Parent = parent
	return holder, circle
end

local function spinner(parent: Instance, color: Color3, size: number, order: number): (Frame, Frame)
	local holder = blank("Spinner", order)
	holder.Size = UDim2.fromOffset(size, size)
	local ring = blank("Ring")
	ring.AnchorPoint = Vector2.new(0.5, 0.5)
	ring.Position = UDim2.fromScale(0.5, 0.5)
	ring.Size = UDim2.fromOffset(size - 8, size - 8)
	corner(ring, -1)
	local edge = stroke(ring, color, 2.5)
	local tail = Instance.new("UIGradient")
	tail.Transparency = NumberSequence.new({
		NumberSequenceKeypoint.new(0, 0),
		NumberSequenceKeypoint.new(0.5, 0.1),
		NumberSequenceKeypoint.new(1, 1),
	})
	tail.Parent = edge
	ring.Parent = holder
	holder.Parent = parent
	return holder, ring
end

local function brandGradient(parent: Instance, rotation: number): UIGradient
	local item = Instance.new("UIGradient")
	item.Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0, PALETTE.cyan),
		ColorSequenceKeypoint.new(0.5, PALETTE.violet),
		ColorSequenceKeypoint.new(1, PALETTE.pink),
	})
	item.Rotation = rotation
	item.Parent = parent
	return item
end

local GRID_REACH = Vector2.new(300, 420)
local function gridLines(parent: Instance, strength: number, cell: number)
	local wide = math.floor(GRID_REACH.X * 0.75)
	local deep = math.floor(GRID_REACH.Y * 0.75)
	local holder = blank("Grid")
	holder.AnchorPoint = Vector2.new(0.5, 0)
	holder.Position = UDim2.fromScale(0.5, 0)
	holder.Size = UDim2.fromOffset(2 * wide, deep)
	local function mask(x: number, y: number): number
		local reach = math.sqrt((x / GRID_REACH.X) ^ 2 + (y / GRID_REACH.Y) ^ 2)
		return math.clamp((0.75 - reach) / 0.45, 0, 1)
	end
	local function line(name: string, position: UDim2, size: UDim2, rotation: number, along: (number) -> number)
		local keys = {}
		local most = 0
		for step = 0, 10 do
			local shows = along(step / 10)
			most = math.max(most, shows)
			table.insert(keys, NumberSequenceKeypoint.new(step / 10, 1 - shows))
		end
		if most < 0.05 then
			return
		end
		local item = Instance.new("Frame")
		item.Name = name
		item.BackgroundColor3 = WHITE
		item.BackgroundTransparency = 1 - strength
		item.BorderSizePixel = 0
		item.Position = position
		item.Size = size
		local fade = Instance.new("UIGradient")
		fade.Rotation = rotation
		fade.Transparency = NumberSequence.new(keys)
		fade.Parent = item
		item.Parent = holder
	end
	local x = cell // 2
	while x < wide do
		for _, side in { -1, 1 } do
			local from = side * x
			line("GridColumn", UDim2.fromOffset(wide + from, 0), UDim2.fromOffset(1, deep), 90, function(t: number): number
				return mask(from, t * deep)
			end)
		end
		x += cell
	end
	local y = HEADER_HEIGHT - 1 + cell
	while y < deep do
		local at = y
		line("GridRow", UDim2.fromOffset(0, at), UDim2.fromOffset(2 * wide, 1), 0, function(t: number): number
			return mask((t - 0.5) * 2 * wide, at)
		end)
		y += cell
	end
	holder.Parent = parent
end

local function clipboard(parent: Instance, color: Color3): Frame
	local holder = blank("Icon")
	holder.Size = UDim2.fromOffset(20, 24)
	local board = blank("Board")
	board.Position = UDim2.fromOffset(1, 3)
	board.Size = UDim2.fromOffset(18, 21)
	corner(board, 4)
	stroke(board, color, 1.5)
	board.Parent = holder
	local clip = Instance.new("Frame")
	clip.Name = "Clip"
	clip.Position = UDim2.fromOffset(5, 0)
	clip.Size = UDim2.fromOffset(10, 5)
	clip.BackgroundColor3 = color
	clip.BorderSizePixel = 0
	clip.ZIndex = 2
	corner(clip, 2)
	clip.Parent = holder
	for index, y in { 11, 16 } do
		local line = Instance.new("Frame")
		line.Name = "Line" .. index
		line.Position = UDim2.fromOffset(6, y)
		line.Size = UDim2.fromOffset(if index == 1 then 8 else 5, 2)
		line.BackgroundColor3 = color
		line.BackgroundTransparency = 0.35
		line.BorderSizePixel = 0
		corner(line, 1)
		line.Parent = holder
	end
	holder.Parent = parent
	return holder
end

local function gear(parent: Instance, color: Color3)
	local holder = blank("Icon")
	holder.AnchorPoint = Vector2.new(0.5, 0.5)
	holder.Position = UDim2.fromScale(0.5, 0.5)
	holder.Size = UDim2.fromOffset(16, 16)
	local ring = blank("Ring")
	ring.AnchorPoint = Vector2.new(0.5, 0.5)
	ring.Position = UDim2.fromScale(0.5, 0.5)
	ring.Size = UDim2.fromOffset(8, 8)
	corner(ring, -1)
	stroke(ring, color, 2)
	ring.Parent = holder
	for index = 0, 7 do
		local angle = math.rad(index * 45)
		local tooth = Instance.new("Frame")
		tooth.Name = "Tooth"
		tooth.AnchorPoint = Vector2.new(0.5, 0.5)
		tooth.Position = UDim2.new(0.5, math.cos(angle) * 6, 0.5, math.sin(angle) * 6)
		tooth.Size = UDim2.fromOffset(4, 3)
		tooth.Rotation = index * 45
		tooth.BackgroundColor3 = color
		tooth.BorderSizePixel = 0
		corner(tooth, 1)
		tooth.Parent = holder
	end
	holder.Parent = parent
end

local function key(parent: Instance, text: string, colors: Colors, order: number): TextLabel
	local item = Instance.new("TextLabel")
	item.Name = "Key"
	item.AutomaticSize = Enum.AutomaticSize.X
	item.Size = UDim2.fromOffset(0, 20)
	item.BackgroundColor3 = colors.raised
	item.BorderSizePixel = 0
	item.FontFace = font(Enum.FontWeight.SemiBold)
	item.TextSize = 12
	item.TextColor3 = colors.text
	item.Text = text
	item.LayoutOrder = order
	padding(item, 0, 6, 0, 6)
	corner(item, 5)
	stroke(item, colors.lineStrong)
	item.Parent = parent
	return item
end

local function keys(parent: Instance, names: { string }, colors: Colors, order: number): Frame
	local row = blank("Keys", order)
	row.AutomaticSize = Enum.AutomaticSize.XY
	row.Size = UDim2.new()
	local layout = list(row, 4, true)
	layout.VerticalAlignment = Enum.VerticalAlignment.Center
	for index, name in names do
		if index > 1 then
			local plus = Instance.new("TextLabel")
			plus.Name = "Plus"
			plus.BackgroundTransparency = 1
			plus.AutomaticSize = Enum.AutomaticSize.X
			plus.Size = UDim2.fromOffset(0, 20)
			plus.FontFace = font(Enum.FontWeight.Medium)
			plus.TextSize = 12
			plus.TextColor3 = colors.dim
			plus.Text = "+"
			plus.LayoutOrder = index * 2 - 1
			plus.Parent = row
		end
		key(row, name, colors, index * 2)
	end
	row.Parent = parent
	return row
end

local function shown(item: Instance, root: Instance, leaving: { [GuiObject]: {} }): boolean
	local current: Instance? = item
	while current and current ~= root do
		if current:IsA("GuiObject") and (not current.Visible or leaving[current] ~= nil) then
			return false
		end
		current = current.Parent
	end
	return current == root
end

local function interactable(item: GuiObject, on: boolean)
	pcall(function()
		item.Interactable = on
	end)
end

local FADE_TEXT = { "BackgroundTransparency", "TextTransparency" }
local FADE_IMAGE = { "BackgroundTransparency", "ImageTransparency" }
local FADE_SCROLL = { "BackgroundTransparency", "ScrollBarImageTransparency" }
local FADE_BOX = { "BackgroundTransparency" }
local FADE_LINE = { "Transparency" }
local FADE_NONE: { string } = {}
local function fadeProperties(item: Instance): { string }
	if item:IsA("TextButton") and item.Text == "" then
		return FADE_BOX
	elseif item:IsA("TextLabel") or item:IsA("TextButton") or item:IsA("TextBox") then
		return FADE_TEXT
	elseif item:IsA("ImageLabel") or item:IsA("ImageButton") then
		return FADE_IMAGE
	elseif item:IsA("ScrollingFrame") then
		return FADE_SCROLL
	elseif item:IsA("GuiObject") then
		return FADE_BOX
	elseif item:IsA("UIStroke") or item.ClassName == SHADOW_CLASS then
		return FADE_LINE
	end
	return FADE_NONE
end

local function reducedMotion(info: Info): boolean
	local forced = info.reducedMotion
	if forced ~= nil then
		return forced
	end
	local still = false
	pcall(function()
		still = GuiService.ReducedMotionEnabled == true
	end)
	return still
end

function Ui.new(parent: Instance, handlers: Handlers, info: Info): Ui
	local root = Instance.new("Frame")
	root.Name = "NKD_Window"
	root.Size = UDim2.fromScale(1, 1)
	root.BorderSizePixel = 0
	root.ClipsDescendants = true
	local self = setmetatable({
		root = root,
		handlers = handlers,
		info = info,
		colors = readColors(),
		state = { view = "home", title = "" } :: State,
		settingsState = nil :: SettingsState?,
		notices = {} :: { Notice },
		page = "main",
		actions = {} :: { [GuiObject]: () -> () },
		looks = {} :: { [GuiObject]: (state: string) -> () },
		connections = {} :: { RBXScriptConnection },
		parts = {} :: { [string]: GuiObject },
		loops = {} :: { [string]: Tween },
		resting = {} :: { [Instance]: { [string]: FadeEntry } },
		leaving = {} :: { [GuiObject]: {} },
		progressShown = 0,
		spinAngle = 0,
		segmentsShown = {} :: { boolean },
		noticeIds = {} :: { string },
		revealedAt = -math.huge,
		still = reducedMotion(info),
		awake = false,
		pop = false,
		pasteLook = nil :: (() -> ())?,
		destroyed = false,
	}, Ui)
	self:render()
	root.Parent = parent
	if parent:IsA("LayerCollector") then
		table.insert(
			self.connections,
			parent:GetPropertyChangedSignal("Enabled"):Connect(function()
				self:wake()
			end)
		)
	end
	table.insert(
		self.connections,
		root.AncestryChanged:Connect(function()
			self:wake()
		end)
	)
	pcall(function()
		table.insert(
			self.connections,
			root.Destroying:Connect(function()
				self:stop()
			end)
		)
	end)
	if info.reducedMotion == nil then
		pcall(function()
			table.insert(
				self.connections,
				GuiService:GetPropertyChangedSignal("ReducedMotionEnabled"):Connect(function()
					self:setStill(reducedMotion(info))
				end)
			)
		end)
	end
	task.defer(function()
		self:wake()
	end)
	return self
end

function Ui.tween(self: Ui, item: Instance, goal: { [string]: any }, info: TweenInfo?)
	if self.still then
		self:snap(item, goal)
		return
	end
	TweenService:Create(item, info or QUICK, goal):Play()
end

function Ui.snap(self: Ui, item: Instance, goal: { [string]: any })
	TweenService:Create(item, INSTANT, goal):Play()
	for name, value in goal do
		(item :: any)[name] = value
	end
end

function Ui.states(self: Ui, item: GuiButton, look: (state: string) -> ())
	local inside, selected, down = false, false, false
	local current = "normal"
	local function update()
		local now = if (inside or selected) and down then "pressed" elseif inside or selected then "hover" else "normal"
		if now ~= current then
			current = now
			look(now)
		end
	end
	item.MouseEnter:Connect(function()
		inside = true
		update()
	end)
	item.MouseLeave:Connect(function()
		inside = false
		down = false
		update()
	end)
	item.SelectionGained:Connect(function()
		selected = true
		update()
	end)
	item.SelectionLost:Connect(function()
		selected = false
		down = false
		update()
	end)
	item.MouseButton1Down:Connect(function()
		down = true
		update()
	end)
	item.MouseButton1Up:Connect(function()
		down = false
		update()
	end)
	self.looks[item] = function(state: string)
		selected = false
		inside = state == "hover" or state == "pressed"
		down = state == "pressed"
		if state == "rest" then
			current = "normal"
			look("rest")
		else
			update()
		end
	end
end

function Ui.react(self: Ui, item: GuiButton, normal: { [string]: any }, hover: { [string]: any }, pressed: { [string]: any }, target: Instance?)
	local looks: Instance = target or item
	self:states(item, function(state: string)
		if state == "rest" then
			self:snap(looks, normal)
		else
			self:tween(looks, if state == "hover" then hover elseif state == "pressed" then pressed else normal)
		end
	end)
end

function Ui.rest(self: Ui, item: Instance)
	for button, look in self.looks do
		if button:IsDescendantOf(item) then
			look("rest")
		end
	end
end

function Ui.isOpen(self: Ui): boolean
	local host = self.root.Parent
	if host == nil or not self.root:IsDescendantOf(game) then
		return false
	end
	if host:IsA("LayerCollector") then
		return host.Enabled
	end
	return true
end

function Ui.wake(self: Ui)
	if self.destroyed then
		return
	end
	local open = self:isOpen()
	if open == self.awake then
		return
	end
	self.awake = open
	if open then
		self:openPage(self.page, true)
	end
	self:motion()
end

function Ui.setStill(self: Ui, still: boolean)
	if still == self.still or self.destroyed then
		return
	end
	self.still = still
	local ring: GuiObject? = self.parts.SpinnerRing
	if still then
		self:setLoop("spinner", nil)
	elseif ring and ring:IsDescendantOf(self.root) then
		self:spin(ring)
	end
	self:drift()
	self:motion()
end

function Ui.spin(self: Ui, ring: GuiObject)
	local from = ring.Rotation % 360
	ring.Rotation = from
	local turn = TweenInfo.new(SPIN, Enum.EasingStyle.Linear, Enum.EasingDirection.In, -1)
	self:setLoop("spinner", TweenService:Create(ring, turn, { Rotation = from + 360 }))
end

function Ui.setLoop(self: Ui, key: string, tween: Tween?)
	local old = self.loops[key]
	if old then
		old:Cancel()
	end
	if tween == nil then
		self.loops[key] = nil
		return
	end
	self.loops[key] = tween
	if self.awake and not self.destroyed then
		tween:Play()
	end
end

function Ui.motion(self: Ui)
	local playing = self.awake and not self.destroyed
	for _, tween in self.loops do
		if playing then
			tween:Play()
		else
			tween:Pause()
		end
	end
end

function Ui.stop(self: Ui)
	for key in self.loops do
		self:setLoop(key, nil)
	end
end

function Ui.drift(self: Ui)
	for _, light in BACKDROP.lights do
		self:setLoop(light.name, nil)
		local blob = self.parts[light.name]
		if blob then
			blob.Position = light.at
			if not self.still and light.drift.Magnitude > 0 and light.seconds > 0 then
				local away = light.at + UDim2.fromOffset(light.drift.X, light.drift.Y)
				local info = TweenInfo.new(light.seconds, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut, -1, true)
				self:setLoop(light.name, TweenService:Create(blob, info, { Position = away }))
			end
		end
	end
end

function Ui.built(self: Ui, object: Instance, name: string): number
	local rest = self.resting[object]
	local kept = if rest then rest[name] else nil
	return if kept then kept.value else (object :: any)[name]
end

function Ui.tint(self: Ui, icon: Instance, color: Color3)
	for _, item in icon:GetDescendants() do
		if item:IsA("UIStroke") then
			item.Color = color
		elseif item:IsA("Frame") and self:built(item, "BackgroundTransparency") < 1 then
			item.BackgroundColor3 = color
		end
	end
end

function Ui.fadeTargets(self: Ui, item: Instance, deep: boolean, owner: {}): { FadeTarget }
	local targets: { FadeTarget } = {}
	local function visit(object: Instance)
		for _, name in fadeProperties(object) do
			local built = self:built(object, name)
			if built < 1 then
				local rest = self.resting[object] or {}
				self.resting[object] = rest
				rest[name] = { value = built, owner = owner }
				table.insert(targets, { object = object, name = name, built = built })
			end
		end
	end
	visit(item)
	if deep then
		for _, object in item:GetDescendants() do
			visit(object)
		end
	end
	return targets
end

function Ui.fadeTo(self: Ui, targets: { FadeTarget }, visibility: number, info: TweenInfo?, owner: {}?)
	for _, target in targets do
		if owner then
			local rest = self.resting[target.object]
			local kept = if rest then rest[target.name] else nil
			if kept == nil or kept.owner ~= owner then
				continue
			end
		end
		local value = 1 - (1 - target.built) * visibility
		if info == nil or self.still then
			self:snap(target.object, { [target.name] = value })
		else
			TweenService:Create(target.object, info, { [target.name] = value }):Play()
		end
	end
end

function Ui.restore(self: Ui, owner: {})
	for object, rest in self.resting do
		if object:IsDescendantOf(self.root) then
			for name, kept in rest do
				if kept.owner == owner then
					self:snap(object, { [name] = kept.value })
				end
			end
		end
	end
	self:settle(owner)
end

function Ui.settle(self: Ui, owner: {})
	for object, rest in self.resting do
		for name, kept in rest do
			if kept.owner == owner then
				rest[name] = nil
			end
		end
		if next(rest) == nil then
			self.resting[object] = nil
		end
	end
end

function Ui.fadeIn(self: Ui, item: GuiObject, options: FadeOptions?)
	if self.still or not self.awake then
		return
	end
	local choice: FadeOptions = options or {}
	local length = choice.time or REVEAL
	local done = math.clamp((choice.since or 0) / length, 0, 1)
	if done >= 1 then
		return
	end
	local start = 1 - (1 - done) ^ 5
	local owner = {}
	local targets = self:fadeTargets(item, choice.shallow ~= true, owner)
	self:fadeTo(targets, start)
	local pad: UIPadding? = if choice.slide then item:FindFirstChildOfClass("UIPadding") else nil
	local top, bottom = UDim.new(), UDim.new()
	if pad then
		top, bottom = pad.PaddingTop, pad.PaddingBottom
		local shift = math.floor(math.max(0, math.min(SLIDE, bottom.Offset)) * (1 - start) + 0.5)
		pad.PaddingTop = UDim.new(top.Scale, top.Offset + shift)
		pad.PaddingBottom = UDim.new(bottom.Scale, bottom.Offset - shift)
	end
	local info = TweenInfo.new(length * (1 - done), Enum.EasingStyle.Quint, Enum.EasingDirection.Out)
	local function play()
		if self.destroyed or not item:IsDescendantOf(self.root) then
			self:settle(owner)
			return
		end
		self:fadeTo(targets, 1, info, owner)
		if pad then
			TweenService:Create(pad, info, { PaddingTop = top, PaddingBottom = bottom }):Play()
		end
		task.delay(info.Time + 0.05, function()
			self:settle(owner)
		end)
	end
	local delay = choice.delay or 0
	if delay > 0 then
		task.delay(delay, play)
	else
		play()
	end
end

function Ui.prune(self: Ui)
	for item in self.actions do
		if not item:IsDescendantOf(self.root) then
			self.actions[item] = nil
		end
	end
	for item in self.looks do
		if not item:IsDescendantOf(self.root) then
			self.looks[item] = nil
		end
	end
	for object in self.resting do
		if not object:IsDescendantOf(self.root) then
			self.resting[object] = nil
		end
	end
end

export type ButtonOptions = {
	enabled: boolean?,
	full: boolean?,
	muted: boolean?,
	height: number?,
	icon: string?,
}

function Ui.button(self: Ui, parent: Instance, name: string, text: string, kind: string, order: number, action: () -> (), options: ButtonOptions?): TextButton
	local colors = self.colors
	local choice: ButtonOptions = options or {}
	local primary = kind == "primary"
	local muted = primary and choice.muted == true
	local enabled = choice.enabled ~= false
	local height = choice.height or 36
	local radius = math.floor(height * 0.3 + 0.5)
	local size = if primary then 15 else 14
	local inset = if primary then 16 else 14
	local sign = if enabled and not muted then choice.icon else nil
	local signRoom = if sign then ICON_GAP + ICON_SIZE else 0
	local wide = textWidth(text, size)

	local item = Instance.new("TextButton")
	item.Name = name
	item.AutoButtonColor = false
	item.BackgroundTransparency = 1
	item.BorderSizePixel = 0
	item.Text = ""
	item.LayoutOrder = order
	if choice.full then
		item.Size = UDim2.new(1, 0, 0, height)
	else
		item.Size = UDim2.fromOffset(wide + 2 * inset + signRoom, height)
		local grow = Instance.new("UIFlexItem")
		grow.FlexMode = Enum.UIFlexMode.Custom
		grow.GrowRatio = if primary then 3 else 1
		grow.ShrinkRatio = 1
		grow.Parent = item
	end

	local face = Instance.new("Frame")
	face.Name = "Face"
	face.AnchorPoint = Vector2.new(0.5, 0.5)
	face.Position = UDim2.fromScale(0.5, 0.5)
	face.Size = UDim2.fromScale(1, 1)
	face.BackgroundColor3 = WHITE
	face.BorderSizePixel = 0
	corner(face, radius)
	local press = Instance.new("UIScale")
	press.Name = "Press"
	press.Parent = face

	local caption = Instance.new("TextLabel")
	caption.Name = "Label"
	caption.BackgroundTransparency = 1
	caption.AnchorPoint = Vector2.new(0.5, 0.5)
	caption.ZIndex = 3
	caption.FontFace = font(if primary then Enum.FontWeight.Bold else Enum.FontWeight.SemiBold)
	caption.TextSize = size
	caption.TextWrapped = true
	caption.TextColor3 = colors.strong
	caption.Text = text
	caption.Parent = face
	local function place(withSign: boolean)
		local room = if withSign then signRoom else 0
		caption.Position = UDim2.new(0.5, -room // 2, 0.5, 0)
		caption.Size = UDim2.new(1, -2 * (inset - 4) - room, 1, 0)
	end
	place(sign ~= nil)
	local glyph: TextLabel? = nil
	if sign then
		local holder = blank("Icon")
		holder.AnchorPoint = Vector2.new(0, 0.5)
		holder.Position = UDim2.new(0.5, wide // 2 - signRoom // 2 + ICON_GAP, 0.5, 0)
		holder.Size = UDim2.fromOffset(ICON_SIZE, height)
		holder.ZIndex = 3
		local mark = Instance.new("TextLabel")
		mark.Name = "Glyph"
		mark.BackgroundTransparency = 1
		mark.Size = UDim2.fromScale(1, 1)
		mark.ZIndex = 3
		mark.FontFace = font(Enum.FontWeight.Bold)
		mark.TextSize = size + 1
		mark.Text = sign
		mark.Parent = holder
		holder.Parent = face
		glyph = mark
		local need = wide + 2 * inset + signRoom
		local function fit()
			local width = item.AbsoluteSize.X
			local fits = width == 0 or width >= need - 1
			holder.Visible = fits
			place(fits)
		end
		item:GetPropertyChangedSignal("AbsoluteSize"):Connect(fit)
		fit()
	end

	local lifted = UDim2.new(0.5, 0, 0.5, -LIFT)
	local level = UDim2.fromScale(0.5, 0.5)
	local look: ((state: string) -> ())? = nil
	if primary and not muted then
		local shade = Instance.new("UIGradient")
		shade.Color = ColorSequence.new({
			ColorSequenceKeypoint.new(0, PALETTE.button[1]),
			ColorSequenceKeypoint.new(0.45, PALETTE.button[2]),
			ColorSequenceKeypoint.new(1, PALETTE.button[3]),
		})
		shade.Rotation = 45
		shade.Parent = face
		local edge = inside(stroke(face, WHITE, 1, 0.2))
		litFromAbove(edge, 0.575, 0.85)
		local sheen = Instance.new("Frame")
		sheen.Name = "Sheen"
		sheen.BackgroundColor3 = WHITE
		sheen.BorderSizePixel = 0
		sheen.Size = UDim2.fromScale(1, 1)
		sheen.ZIndex = 1
		corner(sheen, radius)
		local wash = Instance.new("UIGradient")
		wash.Rotation = 90
		wash.Transparency = NumberSequence.new({
			NumberSequenceKeypoint.new(0, 0.84),
			NumberSequenceKeypoint.new(0.48, 1),
			NumberSequenceKeypoint.new(1, 1),
		})
		wash.Parent = sheen
		sheen.Parent = face
		caption.TextColor3 = PALETTE.buttonText
		if enabled then
			local sweep = Instance.new("Frame")
			sweep.Name = "Sweep"
			sweep.BackgroundColor3 = WHITE
			sweep.BorderSizePixel = 0
			sweep.Size = UDim2.fromScale(1, 1)
			sweep.ZIndex = 2
			sweep.Visible = false
			corner(sweep, radius)
			local band = Instance.new("UIGradient")
			band.Name = "Band"
			band.Transparency = NumberSequence.new({
				NumberSequenceKeypoint.new(0, 1),
				NumberSequenceKeypoint.new(0.3, 1),
				NumberSequenceKeypoint.new(0.48, 0.72),
				NumberSequenceKeypoint.new(0.66, 1),
				NumberSequenceKeypoint.new(1, 1),
			})
			band.Offset = Vector2.new(-1.2, 0)
			band.Parent = sweep
			sweep.Parent = face
			local glow = softShadow(face, PALETTE.pink, GLOW_REST.Transparency, 28, 10, -24, -1)
			softShadow(face, BLACK, 0.4, 6, 2, -4, -2)

			local sweeping: Tween? = nil
			local function stopSweep()
				local running = sweeping
				sweeping = nil
				if running then
					running:Cancel()
				end
				sweep.Visible = false
				band.Offset = Vector2.new(-1.2, 0)
			end
			local function startSweep()
				stopSweep()
				if self.still then
					return
				end
				local box = face.AbsoluteSize
				band.Rotation = if box.X > 0 and box.Y > 0 then math.deg(math.atan(math.tan(math.rad(25)) * box.Y / box.X)) else 5
				sweep.Visible = true
				local tween = TweenService:Create(band, SWEEP, { Offset = Vector2.new(1.2, 0) })
				sweeping = tween
				tween.Completed:Connect(function()
					if sweeping == tween then
						sweeping = nil
						sweep.Visible = false
					end
				end)
				tween:Play()
			end
			local last = "normal"
			look = function(state: string)
				local before = last
				last = if state == "rest" then "normal" else state
				if state == "rest" then
					stopSweep()
					self:snap(face, { Position = level })
					self:snap(press, { Scale = 1 })
					self:snap(edge, { Transparency = 0.2 })
					if glow then
						self:snap(glow, GLOW_REST)
					end
					if glyph then
						self:snap(glyph, { Position = UDim2.new() })
					end
				elseif state == "pressed" then
					self:tween(face, { Position = level }, PRESS)
					self:tween(press, { Scale = PRESSED }, PRESS)
				else
					local hover = state == "hover"
					self:tween(face, { Position = if hover then lifted else level }, SPRING)
					self:tween(press, { Scale = 1 }, SPRING)
					self:tween(edge, { Transparency = if hover then 0 else 0.2 }, GLOW)
					if glow then
						self:tween(glow, if hover then GLOW_HOVER else GLOW_REST, GLOW)
					end
					if glyph then
						self:tween(glyph, { Position = if hover then UDim2.fromOffset(3, 0) else UDim2.new() }, NUDGE)
					end
					if not hover then
						stopSweep()
					elseif before == "normal" then
						startSweep()
					end
				end
			end
		else
			face.BackgroundTransparency = 0.6
			caption.TextTransparency = 0.4
		end
	elseif muted then
		local idle = 0.85
		face.BackgroundColor3 = colors.accent
		face.BackgroundTransparency = idle
		inside(stroke(face, colors.accent, 1, 0.75))
		caption.TextColor3 = colors.accent
		if enabled then
			look = function(state: string)
				if state == "rest" then
					self:snap(face, { Position = level, BackgroundTransparency = idle })
					self:snap(press, { Scale = 1 })
				elseif state == "pressed" then
					self:tween(face, { Position = level }, PRESS)
					self:tween(face, { BackgroundTransparency = idle - 0.12 }, PRESS)
					self:tween(press, { Scale = PRESSED }, PRESS)
				else
					local hover = state == "hover"
					self:tween(face, { Position = if hover then lifted else level }, SPRING)
					self:tween(face, { BackgroundTransparency = if hover then idle - 0.06 else idle }, EASE)
					self:tween(press, { Scale = 1 }, SPRING)
				end
			end
		else
			caption.TextTransparency = 0.4
		end
	else
		local glass = Instance.new("UIGradient")
		glass.Color = ColorSequence.new(PALETTE.glass[1], PALETTE.glass[2])
		glass.Rotation = 90
		glass.Transparency = NumberSequence.new(0, 0.12)
		glass.Parent = face
		local rest = 0.38
		face.BackgroundTransparency = rest
		local border = inside(stroke(face, colors.lineStrong, 1, 0))
		litFromAbove(border, 0, 0.35)
		caption.TextColor3 = colors.strong
		if enabled then
			local lit = blank("Edge")
			lit.Size = UDim2.fromScale(1, 1)
			lit.ZIndex = 2
			corner(lit, radius)
			local edge = inside(stroke(lit, WHITE, 1, 1))
			local hues = Instance.new("UIGradient")
			hues.Color = ColorSequence.new(PALETTE.cyan, PALETTE.pink)
			hues.Transparency = NumberSequence.new(0.3, 0.25)
			hues.Rotation = 30
			hues.Parent = edge
			lit.Parent = face
			look = function(state: string)
				if state == "rest" then
					self:snap(face, { Position = level, BackgroundTransparency = rest })
					self:snap(press, { Scale = 1 })
					self:snap(border, { Transparency = 0 })
					self:snap(edge, { Transparency = 1 })
					if glyph then
						self:snap(glyph, { Position = UDim2.new() })
					end
				elseif state == "pressed" then
					self:tween(face, { Position = level }, PRESS)
					self:tween(press, { Scale = PRESSED }, PRESS)
				else
					local hover = state == "hover"
					self:tween(face, { Position = if hover then lifted else level }, SPRING)
					self:tween(press, { Scale = 1 }, SPRING)
					self:tween(face, { BackgroundTransparency = if hover then 0.05 else rest }, EASE)
					self:tween(border, { Transparency = if hover then 1 else 0 }, EASE)
					self:tween(edge, { Transparency = if hover then 0 else 1 }, GLOW)
					if glyph then
						self:tween(glyph, { Position = if hover then UDim2.fromOffset(3, 0) else UDim2.new() }, NUDGE)
					end
				end
			end
		else
			caption.TextTransparency = 0.55
			border.Transparency = 0.5
		end
	end
	if glyph then
		glyph.TextColor3 = caption.TextColor3
	end
	face.Parent = item
	if enabled then
		local reactTo = look
		if reactTo then
			self:states(item, reactTo)
		end
		item.Activated:Connect(function()
			action()
		end)
		self.actions[item] = action
	else
		item.Active = false
	end
	item.Parent = parent
	return item
end

function Ui.iconButton(self: Ui, parent: Instance, name: string, order: number, active: boolean, draw: (GuiObject, Color3) -> (), action: () -> ()): TextButton
	local colors = self.colors
	local item = Instance.new("TextButton")
	item.Name = name
	item.AutoButtonColor = false
	item.BorderSizePixel = 0
	item.Text = ""
	item.Size = UDim2.fromOffset(30, 30)
	item.LayoutOrder = order
	item.BackgroundColor3 = if active then colors.accent else colors.raised
	local idle = if active then (if colors.dark then 0.84 else 0.88) else 1
	item.BackgroundTransparency = idle
	corner(item, 8)
	draw(item, if active then colors.accent else colors.dim)
	self:react(item, { BackgroundTransparency = idle }, { BackgroundTransparency = if active then idle - 0.06 else 0 }, { BackgroundTransparency = if active then idle - 0.12 else 0 })
	item.Activated:Connect(action)
	self.actions[item] = action
	item.Parent = parent
	return item
end

function Ui.choice(self: Ui, parent: Instance, name: string, text: string, selected: boolean, order: number, action: () -> ())
	local colors = self.colors
	local row = Instance.new("TextButton")
	row.Name = name
	row.AutoButtonColor = false
	row.BackgroundColor3 = colors.raised
	row.BackgroundTransparency = 1
	row.BorderSizePixel = 0
	row.Size = UDim2.new(1, 0, 0, 0)
	row.AutomaticSize = Enum.AutomaticSize.Y
	row.Text = ""
	row.LayoutOrder = order
	corner(row, 6)
	padding(row, 6, 8, 6, 8)
	local ring = Instance.new("Frame")
	ring.Name = "Ring"
	ring.Size = UDim2.fromOffset(16, 16)
	ring.Position = UDim2.fromOffset(0, 1)
	ring.BackgroundColor3 = colors.background
	ring.BorderSizePixel = 0
	corner(ring, -1)
	stroke(ring, if selected then colors.accent else colors.lineStrong, if selected then 1.5 else 1)
	if selected then
		local dot = Instance.new("Frame")
		dot.Name = "Dot"
		dot.Size = UDim2.fromOffset(8, 8)
		dot.Position = UDim2.fromScale(0.5, 0.5)
		dot.AnchorPoint = Vector2.new(0.5, 0.5)
		dot.BackgroundColor3 = colors.accent
		dot.BorderSizePixel = 0
		corner(dot, -1)
		dot.Parent = ring
	end
	ring.Parent = row
	local text_ = label(row, "Label", text, 14, if selected then colors.strong else colors.text, if selected then Enum.FontWeight.Medium else Enum.FontWeight.Regular, 1)
	text_.Size = UDim2.new(1, -26, 0, 0)
	text_.Position = UDim2.fromOffset(26, 0)
	self:react(row, { BackgroundTransparency = 1 }, { BackgroundTransparency = 0.4 }, { BackgroundTransparency = 0 })
	row.Activated:Connect(action)
	self.actions[row] = action
	row.Parent = parent
end

function Ui.card(self: Ui, parent: Instance, name: string, order: number, gap: number?): Frame
	local card = stack(parent, name, order, gap or 10)
	card.BackgroundTransparency = 0
	card.BackgroundColor3 = self.colors.surface
	corner(card, 14)
	litFromAbove(stroke(card, WHITE), 0.8, 0.93)
	softShadow(card, BLACK, 0.5, 22, 8)
	padding(card, 14, 14, 14, 14)
	return card
end

function Ui.linkBox(self: Ui, parent: Instance, name: string, url: string, order: number): TextBox
	local colors = self.colors
	local item = Instance.new("TextBox")
	item.Name = name
	item.Size = UDim2.new(1, 0, 0, 0)
	item.AutomaticSize = Enum.AutomaticSize.Y
	item.BackgroundColor3 = colors.background
	item.BorderSizePixel = 0
	item.ClearTextOnFocus = false
	item.TextEditable = false
	item.TextWrapped = true
	item.TextXAlignment = Enum.TextXAlignment.Left
	item.FontFace = font(Enum.FontWeight.Medium)
	item.TextSize = 13
	item.TextColor3 = colors.accent
	item.Text = url
	item.LayoutOrder = order
	corner(item, 8)
	local edge = stroke(item, colors.lineStrong)
	padding(item, 8, 10, 8, 10)
	item.Focused:Connect(function()
		edge.Color = colors.accent
		task.defer(function()
			if item.Parent then
				item.SelectionStart = 1
				item.CursorPosition = #item.Text + 1
			end
		end)
	end)
	item.FocusLost:Connect(function()
		edge.Color = colors.lineStrong
	end)
	item.Parent = parent
	return item
end

function Ui.mark(self: Ui, parent: Instance): ImageLabel?
	local icon = Brand.toolbarIcon or ""
	if icon == "" then
		return nil
	end
	local logo = Instance.new("ImageLabel")
	logo.Name = "Logo"
	logo.BackgroundTransparency = 1
	logo.BorderSizePixel = 0
	logo.AnchorPoint = Vector2.new(0.5, 0.5)
	logo.Position = UDim2.new(0, LOGO // 2, 0.5, 0)
	logo.Size = UDim2.fromOffset(LOGO, LOGO)
	logo.ScaleType = Enum.ScaleType.Fit
	logo.Image = icon
	logo.MouseEnter:Connect(function()
		if not self.still then
			self:tween(logo, { Rotation = -6, Size = UDim2.fromOffset(LOGO + 2, LOGO + 2) }, EASE)
		end
	end)
	logo.MouseLeave:Connect(function()
		self:tween(logo, { Rotation = 0, Size = UDim2.fromOffset(LOGO, LOGO) }, EASE)
	end)
	logo.Parent = parent
	return logo
end

function Ui.render(self: Ui)
	if self.destroyed then
		return
	end
	local root = self.root
	self:stop()
	for _, child in root:GetChildren() do
		child:Destroy()
	end
	table.clear(self.actions)
	table.clear(self.looks)
	table.clear(self.parts)
	table.clear(self.resting)
	table.clear(self.leaving)
	root.BackgroundColor3 = self.colors.background
	self:renderBackdrop()

	local body = blank("Body")
	body.Position = UDim2.fromOffset(0, HEADER_HEIGHT)
	body.Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT - FOOTER_HEIGHT)
	body.Parent = root
	self:makePage(body, "Main")
	self:makePage(body, "Settings")
	self:makePage(body, "Help")
	self:renderNotices()
	self:renderPaste()
	self:renderCard()
	self:renderMore()
	self:renderSettings()
	self:renderHelp()
	self:renderFooter()
	self:showPage(self.page)
end

function Ui.renderBackdrop(self: Ui)
	local backdrop = blank("Backdrop")
	backdrop.Size = UDim2.fromScale(1, 1)
	backdrop.ZIndex = 0
	for _, light in BACKDROP.lights do
		local blob = blank(light.name)
		blob.AnchorPoint = Vector2.new(0.5, 0.5)
		blob.Position = light.at
		blob.Size = UDim2.fromOffset(light.size.X, light.size.Y)
		corner(blob, -1)
		for index, glow in light.glows do
			softShadow(blob, glow.color, glow.transparency, glow.blur, 0, glow.spread, index - #light.glows - 1)
		end
		blob.Parent = backdrop
		self.parts[light.name] = blob
	end
	if BACKDROP.grid > 0 then
		gridLines(backdrop, BACKDROP.grid, BACKDROP.cell)
	end
	backdrop.Parent = self.root
	self.parts.Backdrop = backdrop
	self:drift()
end

function Ui.renderFooter(self: Ui)
	local info = self.info
	local footer = blank("Footer")
	footer.AnchorPoint = Vector2.new(0, 1)
	footer.Position = UDim2.fromScale(0, 1)
	footer.Size = UDim2.new(1, 0, 0, FOOTER_HEIGHT)
	local words = info.pluginName .. " v" .. (info.version or Brand.pluginVersion) .. "  ·  " .. (info.copyright or Brand.copyright)
	local text = label(footer, "Text", words, 11, self.colors.dim, Enum.FontWeight.Regular, 1)
	text.AnchorPoint = Vector2.new(0.5, 0.5)
	text.Position = UDim2.fromScale(0.5, 0.5)
	text.Size = UDim2.new(1, -2 * PAD, 0, 0)
	text.TextXAlignment = Enum.TextXAlignment.Center
	text.TextWrapped = false
	text.TextTruncate = Enum.TextTruncate.AtEnd
	footer.Parent = self.root
	self.parts.Footer = text
end

function Ui.noticeSteps(self: Ui, parent: Instance, steps: { string }, link: string?, order: number)
	local colors = self.colors
	local holder = stack(parent, "Steps", order, 8)
	for index, step in steps do
		local row = stack(holder, "Step" .. index, index, 6)
		local line = blank("Line", 1)
		line.Size = UDim2.new(1, 0, 0, 0)
		line.AutomaticSize = Enum.AutomaticSize.Y
		badge(line, "Number", tostring(index), colors.text, 20, 1)
		local text = label(line, "Text", step, 13, colors.text, Enum.FontWeight.Regular, 2)
		text.Position = UDim2.fromOffset(30, 1)
		text.Size = UDim2.new(1, -30, 0, 0)
		line.Parent = row
		if index == 1 and link then
			local indent = blank("LinkRow", 2)
			indent.Size = UDim2.new(1, 0, 0, 0)
			indent.AutomaticSize = Enum.AutomaticSize.Y
			padding(indent, 0, 0, 0, 30)
			self:linkBox(indent, "Link", link, 1)
			indent.Parent = row
		end
	end
end

function Ui.renderNotices(self: Ui)
	local old: GuiObject? = self.parts.Notices
	if old then
		old:Destroy()
	end
	local column: GuiObject? = self.parts.MainColumn
	if column == nil then
		return
	end
	local colors = self.colors
	local holder = stack(column, "Notices", 0, 8)
	holder.Visible = #self.notices > 0
	self.parts.Notices = holder
	local before = self.noticeIds
	local ids: { string } = {}
	for index, notice in self.notices do
		local warn = notice.tone == "warn"
		local box = stack(holder, "Notice" .. index, index, 6)
		box.BackgroundTransparency = if warn then (if colors.dark then 0.9 else 0.88) else 0
		box.BackgroundColor3 = if warn then colors.warn else colors.surface
		corner(box, 12)
		if warn then
			stroke(box, colors.warn, 1, 0.7)
		else
			litFromAbove(stroke(box, WHITE), 0.8, 0.93)
		end
		padding(box, 10, 10, 12, 12)
		local head = blank("Head", 1)
		head.Size = UDim2.new(1, 0, 0, 0)
		head.AutomaticSize = Enum.AutomaticSize.Y
		badge(head, "Icon", if warn then "!" else "↑", if warn then colors.warn else colors.text, 20, 1)
		local title = label(head, "Title", notice.title, 13, colors.strong, Enum.FontWeight.Bold, 2)
		title.Position = UDim2.fromOffset(28, 2)
		local id = notice.id
		local buttons = notice.actions
		local own = buttons ~= nil and #buttons > 0
		title.Size = UDim2.new(1, if own then -28 else -56, 0, 0)
		if not own then
			local hide = Instance.new("TextButton")
			hide.Name = "HideNotice"
			hide.AutoButtonColor = false
			hide.AnchorPoint = Vector2.new(1, 0)
			hide.Position = UDim2.fromScale(1, 0)
			hide.Size = UDim2.fromOffset(22, 22)
			hide.BackgroundColor3 = colors.raised
			hide.BackgroundTransparency = 1
			hide.BorderSizePixel = 0
			hide.FontFace = font(Enum.FontWeight.Bold)
			hide.TextSize = 16
			hide.TextColor3 = colors.dim
			hide.Text = "×"
			corner(hide, 6)
			self:react(hide, { BackgroundTransparency = 1 }, { BackgroundTransparency = 0 }, { BackgroundTransparency = 0 })
			local function action()
				self.handlers.hideNotice(id)
			end
			hide.Activated:Connect(action)
			self.actions[hide] = action
			hide.Parent = head
		end
		head.Parent = box
		label(box, "Text", notice.text, 13, colors.text, Enum.FontWeight.Regular, 2)
		local link = notice.link
		local steps = notice.steps
		if steps and #steps > 0 then
			self:noticeSteps(box, steps, link, 3)
		elseif link then
			self:linkBox(box, "Link", link, 3)
		end
		if buttons and own then
			local row = flow(box, "Buttons", 4, 6)
			for index, button in buttons do
				local actionId = button.id
				self:button(row, actionId, button.label, if button.primary then "primary" else "secondary", index, function()
					local handler = self.handlers.noticeAction
					if handler then
						handler(id, actionId)
					end
				end, { height = 28 })
			end
		end
		table.insert(ids, id)
		if not table.find(before, id) and self.page == "main" then
			self:fadeIn(box, { delay = (#ids - 1) * STAGGER, slide = true })
		end
	end
	self.noticeIds = ids
end

function Ui.makePage(self: Ui, parent: Instance, name: string)
	local scroll = Instance.new("ScrollingFrame")
	scroll.Name = name .. "Page"
	scroll.Size = UDim2.fromScale(1, 1)
	scroll.BackgroundTransparency = 1
	scroll.BorderSizePixel = 0
	scroll.CanvasSize = UDim2.new()
	scroll.AutomaticCanvasSize = Enum.AutomaticSize.Y
	scroll.ScrollingDirection = Enum.ScrollingDirection.Y
	scroll.ScrollBarThickness = 4
	scroll.ScrollBarImageColor3 = self.colors.dim
	scroll.ScrollBarImageTransparency = 0.4
	scroll.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar
	scroll.Visible = false
	local layout = list(scroll, 0)
	layout.HorizontalAlignment = Enum.HorizontalAlignment.Center
	local column = blank(name, 1)
	column.Size = UDim2.new(1, 0, 0, 0)
	column.AutomaticSize = Enum.AutomaticSize.Y
	local limit = Instance.new("UISizeConstraint")
	limit.MaxSize = Vector2.new(MAX_WIDTH, math.huge)
	limit.Parent = column
	padding(column, COLUMN_TOP, PAD, COLUMN_BOTTOM, PAD)
	list(column, 12)
	column.Parent = scroll
	scroll.Parent = parent
	self.parts[name .. "Column"] = column
	self.parts[name .. "Page"] = scroll
end

function Ui.openPage(self: Ui, page: string, window: boolean?)
	local prefix = PAGE_PARTS[page]
	if prefix == nil or self.still or not self.awake then
		return
	end
	local scroll: GuiObject? = self.parts[prefix .. "Page"]
	local column: GuiObject? = self.parts[prefix .. "Column"]
	if scroll == nil or column == nil then
		return
	end
	self:rest(if window then self.root else scroll)
	local holder: Instance = column:FindFirstChild("Body") or column
	local sections: { GuiObject } = {}
	for _, child in holder:GetChildren() do
		if child:IsA("GuiObject") and child.Visible then
			table.insert(sections, child)
		end
	end
	table.sort(sections, function(a: GuiObject, b: GuiObject): boolean
		return a.LayoutOrder < b.LayoutOrder
	end)
	for index, section in sections do
		self:fadeIn(section, { delay = (index - 1) * STAGGER })
	end
	self:fadeIn(scroll, { shallow = true })
	local pad = column:FindFirstChildOfClass("UIPadding")
	if pad then
		pad.PaddingTop = UDim.new(0, COLUMN_TOP + RISE)
		pad.PaddingBottom = UDim.new(0, COLUMN_BOTTOM - RISE)
		local rise = TweenInfo.new(REVEAL + STAGGER, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)
		TweenService:Create(pad, rise, { PaddingTop = UDim.new(0, COLUMN_TOP), PaddingBottom = UDim.new(0, COLUMN_BOTTOM) }):Play()
	end
	if window then
		local header, footer, backdrop = self.parts.Header, self.parts.Footer, self.parts.Backdrop
		if header then
			self:fadeIn(header)
		end
		if footer then
			self:fadeIn(footer, { delay = 2 * STAGGER })
		end
		if backdrop then
			self:fadeIn(backdrop, { time = WINDOW_FADE })
		end
	end
end

function Ui.leavePage(self: Ui, page: string)
	local prefix = PAGE_PARTS[page]
	local found: GuiObject? = if prefix then self.parts[prefix .. "Page"] else nil
	if found == nil then
		return
	end
	local scroll: GuiObject = found
	if self.still or not self.awake or not scroll.Visible then
		scroll.Visible = false
		return
	end
	self:rest(scroll)
	local owner = {}
	self.leaving[scroll] = owner
	scroll.ZIndex = 0
	interactable(scroll, false)
	local targets = self:fadeTargets(scroll, true, owner)
	local info = TweenInfo.new(LEAVE, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
	self:fadeTo(targets, 0, info, owner)
	local column: GuiObject? = self.parts[prefix .. "Column"]
	local pad: UIPadding? = if column then column:FindFirstChildOfClass("UIPadding") else nil
	if pad then
		TweenService:Create(pad, info, { PaddingTop = UDim.new(0, COLUMN_TOP - SINK), PaddingBottom = UDim.new(0, COLUMN_BOTTOM + SINK) }):Play()
	end
	task.delay(LEAVE + 0.02, function()
		if self.leaving[scroll] ~= owner then
			return
		end
		self.leaving[scroll] = nil
		scroll.Visible = false
		scroll.ZIndex = 1
		interactable(scroll, true)
		self:restore(owner)
		if pad then
			self:snap(pad, { PaddingTop = UDim.new(0, COLUMN_TOP), PaddingBottom = UDim.new(0, COLUMN_BOTTOM) })
		end
	end)
end

function Ui.followLeave(self: Ui, page: string, first: GuiObject?, second: GuiObject?)
	local prefix = PAGE_PARTS[page]
	local scroll: GuiObject? = if prefix then self.parts[prefix .. "Page"] else nil
	local owner = if scroll then self.leaving[scroll] else nil
	if owner == nil then
		return
	end
	if first then
		self:fadeTo(self:fadeTargets(first, true, owner), 0)
	end
	if second then
		self:fadeTo(self:fadeTargets(second, true, owner), 0)
	end
end

function Ui.showPage(self: Ui, page: string)
	local previous = self.page
	self.page = page
	for name, prefix in PAGE_PARTS do
		local scroll = self.parts[prefix .. "Page"]
		if scroll then
			if name == page then
				self.leaving[scroll] = nil
				scroll.ZIndex = 1
				interactable(scroll, true)
				scroll.Visible = true
			elseif name == previous and previous ~= page then
				self:leavePage(name)
			elseif self.leaving[scroll] == nil then
				scroll.Visible = false
			end
		end
	end
	self:renderHeader()
	if previous ~= page then
		self:openPage(page)
	end
end

function Ui.renderHeader(self: Ui)
	local old = self.parts.Header
	if old then
		old:Destroy()
		self:prune()
	end
	local colors = self.colors
	local header = Instance.new("Frame")
	header.Name = "Header"
	header.BackgroundColor3 = colors.background
	header.BackgroundTransparency = 1
	header.BorderSizePixel = 0
	header.Size = UDim2.new(1, 0, 0, HEADER_HEIGHT)
	local inner = blank("Inner")
	inner.AnchorPoint = Vector2.new(0.5, 0)
	inner.Position = UDim2.fromScale(0.5, 0)
	inner.Size = UDim2.new(1, -2 * PAD, 1, -1)
	local limit = Instance.new("UISizeConstraint")
	limit.MaxSize = Vector2.new(MAX_WIDTH - 2 * PAD, math.huge)
	limit.Parent = inner
	inner.Parent = header
	local logo = self:mark(inner)
	local left = if logo then LOGO + 10 else 0

	local names = blank("Names")
	names.AnchorPoint = Vector2.new(0, 0.5)
	names.Position = UDim2.new(0, left, 0.5, 0)
	names.Size = UDim2.new(1, -left - 72, 0, 0)
	names.AutomaticSize = Enum.AutomaticSize.Y
	list(names, 0)
	local title = label(names, "Title", self.info.pluginName, 16, colors.strong, Enum.FontWeight.Bold, 1)
	title.TextWrapped = false
	title.TextTruncate = Enum.TextTruncate.AtEnd
	local subtitle = label(names, "Subtitle", "for " .. self.info.toolName, 12, colors.dim, Enum.FontWeight.Medium, 2)
	subtitle.TextWrapped = false
	subtitle.TextTruncate = Enum.TextTruncate.AtEnd
	names.Parent = inner

	local buttons = blank("Buttons")
	buttons.AnchorPoint = Vector2.new(1, 0.5)
	buttons.Position = UDim2.fromScale(1, 0.5)
	buttons.Size = UDim2.fromOffset(66, 30)
	local layout = list(buttons, 6, true)
	layout.HorizontalAlignment = Enum.HorizontalAlignment.Right
	self:iconButton(buttons, "HelpToggle", 1, self.page == "help", function(holder: GuiObject, color: Color3)
		local glyph = Instance.new("TextLabel")
		glyph.Name = "Icon"
		glyph.BackgroundTransparency = 1
		glyph.Size = UDim2.fromScale(1, 1)
		glyph.FontFace = font(Enum.FontWeight.Bold)
		glyph.TextSize = 16
		glyph.TextColor3 = color
		glyph.Text = "?"
		glyph.Parent = holder
	end, function()
		self:showPage(if self.page == "help" then "main" else "help")
	end)
	self:iconButton(buttons, "SettingsToggle", 2, self.page == "settings", gear, function()
		self:showPage(if self.page == "settings" then "main" else "settings")
	end)
	buttons.Parent = inner

	local divider = Instance.new("Frame")
	divider.Name = "Divider"
	divider.AnchorPoint = Vector2.new(0, 1)
	divider.Position = UDim2.fromScale(0, 1)
	divider.Size = UDim2.new(1, 0, 0, 1)
	divider.BackgroundColor3 = WHITE
	divider.BorderSizePixel = 0
	local fade = brandGradient(divider, 0)
	fade.Transparency = NumberSequence.new({
		NumberSequenceKeypoint.new(0, 1),
		NumberSequenceKeypoint.new(0.3, 0.62),
		NumberSequenceKeypoint.new(0.7, 0.62),
		NumberSequenceKeypoint.new(1, 1),
	})
	divider.Parent = header
	header.Parent = self.root
	self.parts.Header = header
end

function Ui.renderPaste(self: Ui)
	local old = self.parts.Paste
	if old then
		old:Destroy()
	end
	local colors = self.colors
	local busy = self.state.busy == true
	local holder = stack(self.parts.MainColumn, "Paste", 1, 8)
	self.parts.Paste = holder

	local area = Instance.new("Frame")
	area.Name = "PasteArea"
	area.Size = UDim2.new(1, 0, 0, 128)
	area.BackgroundColor3 = if busy then colors.background else colors.surface
	area.BorderSizePixel = 0
	area.LayoutOrder = 1
	corner(area, 14)
	local edge = stroke(area, colors.lineStrong, 1.5)
	litFromAbove(edge, 0, 0.4)
	softShadow(area, BLACK, 0.5, 22, 8)
	self.parts.PasteArea = area

	local icon = clipboard(area, colors.dim)
	icon.AnchorPoint = Vector2.new(0.5, 0)
	icon.Position = UDim2.new(0.5, 0, 0, 18)
	local hint = keys(area, { "Ctrl", "V" }, colors, 0)
	hint.AnchorPoint = Vector2.new(0.5, 1)
	hint.Position = UDim2.new(0.5, 0, 1, -14)
	hint.Visible = not busy

	local box = Instance.new("TextBox")
	box.Name = "PasteBox"
	box.Size = UDim2.fromScale(1, 1)
	box.BackgroundTransparency = 1
	box.BorderSizePixel = 0
	box.ZIndex = 3
	box.ClearTextOnFocus = true
	box.MultiLine = true
	box.TextWrapped = true
	box.TextTruncate = Enum.TextTruncate.AtEnd
	box.ClipsDescendants = true
	box.TextXAlignment = Enum.TextXAlignment.Center
	box.TextYAlignment = Enum.TextYAlignment.Center
	box.FontFace = font(Enum.FontWeight.SemiBold)
	box.TextSize = 15
	box.TextColor3 = colors.strong
	box.PlaceholderColor3 = if busy then colors.dim else colors.strong
	box.PlaceholderText = if busy then "Working… one moment" else "Paste the code from " .. self.info.toolName .. " here"
	box.Text = ""
	box.TextEditable = not busy
	padding(box, 46, 16, 38, 16)

	local focused = false
	local hovered = false
	local function look()
		if busy then
			return
		end
		edge.Thickness = if focused then 2 else 1.5
		self:tween(edge, { Color = if focused then colors.accent elseif hovered then colors.dim else colors.lineStrong })
		local view = self.state.view
		self:tint(icon, if focused or view == "home" or view == "pasting" then colors.accent else colors.dim)
	end
	self.pasteLook = look
	look()
	box.MouseEnter:Connect(function()
		hovered = true
		look()
	end)
	box.MouseLeave:Connect(function()
		hovered = false
		look()
	end)

	local clearing = false
	local function take()
		local text = box.Text
		if clearing or text == "" then
			return
		end
		clearing = true
		box.Text = ""
		clearing = false
		task.defer(self.handlers.paste, text)
	end
	box:GetPropertyChangedSignal("Text"):Connect(function()
		local text = box.Text
		if not clearing and (hasCode(text) or #text > 100) then
			take()
		end
	end)
	box.FocusLost:Connect(function()
		focused = false
		look()
		take()
	end)
	box.Focused:Connect(function()
		focused = true
		look()
	end)
	box.Parent = area
	area.Parent = holder
	self.parts.PasteBox = box

	local how = label(holder, "PasteHint", "In " .. self.info.toolName .. ": Export, then Copy for the Studio plugin.", 12, colors.dim, Enum.FontWeight.Regular, 2)
	how.TextXAlignment = Enum.TextXAlignment.Center
end

function Ui.renderMore(self: Ui)
	local old = self.parts.More
	if old then
		old:Destroy()
	end
	local busy = self.state.busy == true
	local row = flow(self.parts.MainColumn, "More", 3, 8)
	self.parts.More = row
	self.parts.Import = self:button(row, "ImportFile", "Import file (." .. Brand.bundleExtension .. ")", "secondary", 1, self.handlers.importFile, { enabled = not busy, height = 32 })
	self.parts.Export = self:button(row, "Export", "Export to designer", "secondary", 2, self.handlers.export, { enabled = not busy, height = 32 })
end

function Ui.focusPaste(self: Ui)
	if self.page ~= "main" then
		self:showPage("main")
	end
	local box = self.parts.PasteBox
	if box and box:IsDescendantOf(self.root) and box:IsA("TextBox") and box.TextEditable then
		box:CaptureFocus()
	end
end

function Ui.nudge(self: Ui)
	local area: GuiObject? = self.parts.PasteArea
	if area == nil or self.still then
		return
	end
	local normal = area.BackgroundColor3
	area.BackgroundColor3 = normal:Lerp(self.colors.accent, 0.12)
	self:tween(area, { BackgroundColor3 = normal }, TweenInfo.new(0.6, Enum.EasingStyle.Quad, Enum.EasingDirection.Out))
end

function Ui.pasteText(self: Ui, text: string)
	local box = self.parts.PasteBox
	if box and box:IsA("TextBox") then
		box.Text = text
	end
end

local CARD_PARTS = { "Card", "Recent", "CardTitle", "CardText", "CardLink", "Progress", "SpinnerRing", "Confirm", "Cancel", "Placeholders", "Dismiss", "Settings", "Help" }

function Ui.renderCard(self: Ui)
	local same = not self.pop
	local oldFill: GuiObject? = self.parts.Progress
	local oldRing: GuiObject? = self.parts.SpinnerRing
	self.progressShown = if same and oldFill and oldFill:IsDescendantOf(self.root) then oldFill.Size.X.Scale else 0
	self.spinAngle = if same and oldRing and oldRing:IsDescendantOf(self.root) then oldRing.Rotation % 360 else 0
	self:setLoop("spinner", nil)
	for _, name in CARD_PARTS do
		local old = self.parts[name]
		if old and (name == "Card" or name == "Recent") then
			old:Destroy()
		end
		self.parts[name] = nil
	end
	local state = self.state
	if state.view == "home" then
		self.segmentsShown = {}
		self:renderHome()
		self:revealCard(self.parts.Card, self.parts.Recent)
		return
	end
	local colors = self.colors
	local card = self:card(self.parts.MainColumn, "Card", 2)
	self.parts.Card = card

	local head = blank("Head", 1)
	head.Size = UDim2.new(1, 0, 0, 0)
	head.AutomaticSize = Enum.AutomaticSize.Y
	local headLayout = list(head, 10, true)
	headLayout.VerticalAlignment = Enum.VerticalAlignment.Center
	self:stateIcon(head, state)
	local title = label(head, "Title", state.title, 15, colors.strong, Enum.FontWeight.Bold, 2)
	title.Size = UDim2.new(1, -34, 0, 0)
	head.Parent = card
	self.parts.CardTitle = title

	if state.text and state.text ~= "" then
		self.parts.CardText = label(card, "Text", state.text, 14, colors.text, Enum.FontWeight.Regular, 2)
	end
	local segments = state.segments
	if segments and #segments > 0 and #segments <= 16 then
		local before = if same then self.segmentsShown else {}
		local row = blank("Segments", 3)
		row.Size = UDim2.new(1, 0, 0, 6)
		local layout = list(row, 4, true)
		layout.HorizontalFlex = Enum.UIFlexAlignment.Fill
		for index, pasted in segments do
			local piece = Instance.new("Frame")
			piece.Name = "Part" .. index
			piece.Size = UDim2.new(0, 8, 1, 0)
			piece.BackgroundColor3 = if pasted then colors.accent else colors.raised
			piece.BorderSizePixel = 0
			piece.LayoutOrder = index
			corner(piece, 3)
			piece.Parent = row
			if pasted and before[index] == false then
				piece.BackgroundColor3 = colors.raised
				self:tween(piece, { BackgroundColor3 = colors.accent }, GLOW)
			end
		end
		row.Parent = card
		self.segmentsShown = table.clone(segments)
	elseif state.progress then
		self.segmentsShown = {}
		local track = Instance.new("Frame")
		track.Name = "Progress"
		track.Size = UDim2.new(1, 0, 0, 6)
		track.BackgroundColor3 = colors.raised
		track.BorderSizePixel = 0
		track.LayoutOrder = 3
		corner(track, 3)
		local fill = Instance.new("Frame")
		fill.Name = "Fill"
		fill.BackgroundColor3 = WHITE
		fill.BorderSizePixel = 0
		corner(fill, 3)
		local shade = Instance.new("UIGradient")
		shade.Color = ColorSequence.new({
			ColorSequenceKeypoint.new(0, PALETTE.violet),
			ColorSequenceKeypoint.new(0.55, colors.accent),
			ColorSequenceKeypoint.new(1, colors.accent),
		})
		shade.Parent = fill
		fill.Size = UDim2.fromScale(self.progressShown, 1)
		fill.Parent = track
		track.Parent = card
		self.parts.Progress = fill
		self:tween(fill, { Size = UDim2.fromScale(math.clamp(state.progress, 0, 1), 1) }, GLIDE)
	else
		self.segmentsShown = {}
	end
	local lines = state.lines
	if lines and #lines > 0 then
		local stackOfLines = stack(card, "Lines", 4, 6)
		for index, line in lines do
			local row = blank("Line" .. index, index)
			row.Size = UDim2.new(1, 0, 0, 0)
			row.AutomaticSize = Enum.AutomaticSize.Y
			local dot = Instance.new("Frame")
			dot.Name = "Dot"
			dot.Position = UDim2.fromOffset(2, 7)
			dot.Size = UDim2.fromOffset(5, 5)
			dot.BackgroundColor3 = colors.dim
			dot.BorderSizePixel = 0
			corner(dot, -1)
			dot.Parent = row
			local text = label(row, "Text", line, 14, colors.text, Enum.FontWeight.Regular, 1)
			text.Position = UDim2.fromOffset(16, 0)
			text.Size = UDim2.new(1, -16, 0, 0)
			row.Parent = stackOfLines
		end
	end
	local steps = state.steps
	if steps and #steps > 0 then
		local stackOfSteps = stack(card, "Steps", 5, 8)
		for index, step in steps do
			local row = blank("Step" .. index, index)
			row.Size = UDim2.new(1, 0, 0, 0)
			row.AutomaticSize = Enum.AutomaticSize.Y
			badge(row, "Number", tostring(index), colors.text, 20, 1)
			local text = label(row, "Text", step, 13, colors.text, Enum.FontWeight.Regular, 2)
			text.Position = UDim2.fromOffset(30, 1)
			text.Size = UDim2.new(1, -30, 0, 0)
			row.Parent = stackOfSteps
		end
	end
	local warnings = state.warnings
	if warnings and #warnings > 0 then
		local box = stack(card, "Warnings", 6, 4)
		box.BackgroundTransparency = if colors.dark then 0.9 else 0.88
		box.BackgroundColor3 = colors.warn
		corner(box, 8)
		stroke(box, colors.warn, 1, 0.7)
		padding(box, 8, 10, 8, 10)
		for index, warning in warnings do
			label(box, "Warning" .. index, warning, 13, colors.text, Enum.FontWeight.Regular, index)
		end
	end
	local notes = state.notes
	if notes and #notes > 0 then
		local stackOfNotes = stack(card, "Notes", 7, 4)
		for index, note in notes do
			label(stackOfNotes, "Note" .. index, note, 12, colors.dim, Enum.FontWeight.Regular, index)
		end
	end
	if state.undo then
		local row = blank("Undo", 8)
		row.Size = UDim2.new()
		row.AutomaticSize = Enum.AutomaticSize.XY
		local layout = list(row, 6, true)
		layout.VerticalAlignment = Enum.VerticalAlignment.Center
		local words = label(row, "Text", "Undo with", 12, colors.dim, Enum.FontWeight.Medium, 1)
		words.AutomaticSize = Enum.AutomaticSize.XY
		words.Size = UDim2.new()
		words.TextWrapped = false
		keys(row, { "Ctrl", "Z" }, colors, 2)
		row.Parent = card
	end
	if state.link then
		self.parts.CardLink = self:linkBox(card, "Link", state.link, 9)
	end
	local actions = state.actions
	if actions and #actions > 0 then
		local row = flow(card, "Buttons", 10, 8)
		local handlerOf: { [string]: () -> () } = {
			confirm = self.handlers.confirm,
			cancel = self.handlers.cancel,
			placeholders = self.handlers.placeholders,
			dismiss = self.handlers.dismiss,
			settings = function()
				self:showPage("settings")
			end,
			help = function()
				self:showPage("help")
			end,
		}
		for index, action in actions do
			local handler = handlerOf[action.id]
			if handler then
				local name = string.upper(string.sub(action.id, 1, 1)) .. string.sub(action.id, 2)
				self.parts[name] = self:button(row, name, action.label, if action.primary then "primary" else "secondary", index, handler, {
					icon = if action.primary then "→" else nil,
				})
			end
		end
	end
	self:revealCard(card)
end

function Ui.revealCard(self: Ui, first: GuiObject?, second: GuiObject?)
	if self.page ~= "main" then
		return
	end
	local since = 0
	if self.pop then
		self.revealedAt = os.clock()
	else
		since = os.clock() - self.revealedAt
		if since >= REVEAL then
			return
		end
	end
	if first then
		self:fadeIn(first, { slide = true, since = since })
	end
	if second then
		self:fadeIn(second, { delay = if self.pop then STAGGER else 0, slide = true, since = since })
	end
end

function Ui.stateIcon(self: Ui, parent: Instance, state: State)
	local colors = self.colors
	if state.view == "working" then
		local _, ring = spinner(parent, colors.accent, 24, 1)
		ring.Rotation = self.spinAngle
		self.parts.SpinnerRing = ring
		if not self.still then
			self:spin(ring)
		end
		return
	end
	local glyph, color
	if state.view == "summary" then
		glyph, color = "↓", colors.accent
	elseif state.tone == "ok" then
		glyph, color = "✓", colors.accent
	elseif state.tone == "warn" then
		glyph, color = "!", colors.warn
	elseif state.tone == "error" then
		glyph, color = "!", colors.error
	else
		glyph, color = "i", colors.text
	end
	local holder, circle = badge(parent, "Icon", glyph, color, 24, 1)
	if self.pop and self.awake and not self.still then
		local scale = Instance.new("UIScale")
		scale.Scale = 0.6
		scale.Parent = circle
		self:tween(scale, { Scale = 1 }, POP)
		if state.tone == "ok" then
			task.delay(0.12, function()
				if holder:IsDescendantOf(self.root) then
					self:pulse(holder, color)
				end
			end)
		end
	end
end

function Ui.pulse(self: Ui, holder: GuiObject, color: Color3)
	local ring = blank("Pulse")
	ring.AnchorPoint = Vector2.new(0.5, 0.5)
	ring.Position = UDim2.fromScale(0.5, 0.5)
	ring.Size = UDim2.fromScale(1, 1)
	corner(ring, -1)
	local edge = stroke(ring, color, 2, 0.35)
	local grow = Instance.new("UIScale")
	grow.Scale = 0.8
	grow.Parent = ring
	ring.Parent = holder
	self:tween(grow, { Scale = 1.7 }, PULSE)
	self:tween(edge, { Transparency = 1 }, PULSE)
	task.delay(PULSE.Time + 0.05, function()
		ring:Destroy()
	end)
end

function Ui.renderHome(self: Ui)
	local colors = self.colors
	local state = self.state
	local main = self.parts.MainColumn
	local holder = stack(main, "Card", 2, 8)
	self.parts.Card = holder
	self:button(holder, "ImportIdle", "Import", "primary", 1, function()
		self:focusPaste()
		self:nudge()
	end, { full = true, muted = true, height = 38 })

	local recent = stack(main, "Recent", 4, 8)
	self.parts.Recent = recent
	local lines = state.lines
	if state.title ~= "" or (lines and #lines > 0) then
		local card = self:card(recent, "Last", 1, 4)
		card.BackgroundTransparency = 1
		local heading = label(card, "Title", state.title, 12, colors.dim, Enum.FontWeight.SemiBold, 1)
		self.parts.CardTitle = heading
		if state.text and state.text ~= "" then
			self.parts.CardText = label(card, "Text", state.text, 14, colors.strong, Enum.FontWeight.SemiBold, 2)
		end
		if lines then
			for index, line in lines do
				label(card, "Line" .. index, line, 13, colors.text, Enum.FontWeight.Regular, 2 + index)
			end
		end
	end
	local notes = state.notes
	if notes then
		for index, note in notes do
			local text = label(recent, "Note" .. index, note, 12, colors.dim, Enum.FontWeight.Regular, 1 + index)
			text.TextXAlignment = Enum.TextXAlignment.Center
		end
	end
end

function Ui.pageTitle(self: Ui, parent: Instance, title: string, doneName: string)
	local row = blank("PageTitle", 1)
	row.Size = UDim2.new(1, 0, 0, 32)
	local text = label(row, "Title", title, 16, self.colors.strong, Enum.FontWeight.Bold, 1)
	text.AnchorPoint = Vector2.new(0, 0.5)
	text.Position = UDim2.fromScale(0, 0.5)
	text.Size = UDim2.new(1, -90, 0, 0)
	local done = self:button(row, doneName, "Done", "secondary", 2, function()
		self:showPage("main")
	end, { height = 30 })
	done.AnchorPoint = Vector2.new(1, 0.5)
	done.Position = UDim2.fromScale(1, 0.5)
	row.Parent = parent
end

function Ui.heading(self: Ui, parent: Instance, text: string, order: number): TextLabel
	return label(parent, "Heading", text, 13, self.colors.strong, Enum.FontWeight.Bold, order)
end

function Ui.renderSettings(self: Ui)
	local column = self.parts.SettingsColumn
	local old = self.parts.SettingsBody
	if old then
		old:Destroy()
		self:prune()
	end
	local colors = self.colors
	local body = stack(column, "Body", 1, 12)
	self.parts.SettingsBody = body
	self:pageTitle(body, "Settings", "SettingsDone")
	local settingsState = self.settingsState
	if settingsState == nil then
		return
	end

	local where = self:card(body, "Where", 2, 4)
	self:heading(where, "Where new designs go", 1)
	local targets = stack(where, "Targets", 2, 0)
	for index, choice in settingsState.targetChoices do
		self:choice(targets, "Target_" .. choice.value, choice.label, settingsState.target == choice.value, index, function()
			self.handlers.setTarget(choice.value)
		end)
	end
	label(where, "TargetHint", "Select an earlier import (or a folder) before importing to use that instead.", 12, colors.dim, Enum.FontWeight.Regular, 3)

	local owner = self:card(body, "Owner", 3, 4)
	self:heading(owner, "Uploaded images belong to", 1)
	local creators = stack(owner, "Creators", 2, 0)
	local me = if settingsState.userName then "Me (" .. settingsState.userName .. ")" else "Me"
	self:choice(creators, "Creator_user", me, settingsState.creatorKind == "user", 1, function()
		self.handlers.setCreator("user", settingsState.groupId)
	end)
	self:choice(creators, "Creator_group", "A group", settingsState.creatorKind == "group", 2, function()
		self.handlers.setCreator("group", settingsState.groupId)
	end)
	if settingsState.creatorKind == "group" then
		local box = Instance.new("TextBox")
		box.Name = "GroupId"
		box.Size = UDim2.new(1, 0, 0, 32)
		box.BackgroundColor3 = colors.background
		box.BorderSizePixel = 0
		box.ClearTextOnFocus = false
		box.PlaceholderText = "Group id"
		box.PlaceholderColor3 = colors.dim
		box.Text = if settingsState.groupId then tostring(settingsState.groupId) else ""
		box.TextColor3 = colors.strong
		box.TextXAlignment = Enum.TextXAlignment.Left
		box.TextTruncate = Enum.TextTruncate.AtEnd
		box.FontFace = font(Enum.FontWeight.Regular)
		box.TextSize = 14
		box.LayoutOrder = 3
		corner(box, 8)
		local edge = stroke(box, colors.lineStrong)
		padding(box, 0, 10, 0, 10)
		box.Focused:Connect(function()
			edge.Color = colors.accent
		end)
		box.FocusLost:Connect(function()
			edge.Color = colors.lineStrong
			local digits = string.match(box.Text, "^%s*(%d+)%s*$")
			local id = digits and tonumber(digits)
			self.handlers.setCreator("group", if id and id > 0 then id else nil)
		end)
		box.Parent = owner
		self.parts.GroupId = box
		if settingsState.groupId == nil then
			label(owner, "GroupHint", "Type the group id (the number in the group's link) to upload images to the group.", 12, colors.warn, Enum.FontWeight.Medium, 4)
		end
	end

	local memory = self:card(body, "Memory", 4, 8)
	self:heading(memory, "Upload memory", 1)
	local remembered = if settingsState.uploaded == 1 then "1 uploaded image remembered" else settingsState.uploaded .. " uploaded images remembered"
	label(memory, "Uploaded", remembered .. " on this computer, so each image is uploaded once.", 13, colors.text, Enum.FontWeight.Regular, 2)
	if settingsState.uploaded > 0 then
		local row = flow(memory, "Row", 3, 8)
		self:button(row, "Forget", "Forget uploaded images", "secondary", 1, self.handlers.forgetUploads, { height = 32 })
	end

	local check = settingsState.updateCheck
	if check ~= nil then
		local updates = self:card(body, "Updates", 5, 4)
		self:heading(updates, "Updates", 1)
		local choices = stack(updates, "Choices", 2, 0)
		local function set(on: boolean)
			local handler = self.handlers.setUpdateCheck
			if handler then
				handler(on)
			end
		end
		self:choice(choices, "Updates_on", "Tell me when a new version is out", check, 1, function()
			set(true)
		end)
		self:choice(choices, "Updates_off", "Don't check", not check, 2, function()
			set(false)
		end)
		local site = (string.gsub(Brand.siteOrigin, "^%a+://", ""))
		label(updates, "UpdatesHint", "Once a day it reads a small file from " .. site .. ". Nothing is sent.", 12, colors.dim, Enum.FontWeight.Regular, 3)
	end
end

function Ui.renderHelp(self: Ui)
	local column = self.parts.HelpColumn
	local old = self.parts.HelpBody
	if old then
		old:Destroy()
		self:prune()
	end
	local colors = self.colors
	local tool = self.info.toolName
	local body = stack(column, "Body", 1, 12)
	self.parts.HelpBody = body
	self:pageTitle(body, "Help", "HelpDone")

	local how = self:card(body, "How", 2, 10)
	self:heading(how, "Import a design", 1)
	local steps = {
		"In " .. tool .. ", click Export, then Copy for the Studio plugin.",
		"Click the paste box and press Ctrl+V (Cmd+V on a Mac). A big design comes in parts: paste them one after another.",
		"Check the summary and click Import. Ctrl+Z undoes the whole import.",
	}
	for index, step in steps do
		local row = blank("Step" .. index, 1 + index)
		row.Size = UDim2.new(1, 0, 0, 0)
		row.AutomaticSize = Enum.AutomaticSize.Y
		badge(row, "Number", tostring(index), colors.text, 20, 1)
		local text = label(row, "Text", step, 13, colors.text, Enum.FontWeight.Regular, 2)
		text.Position = UDim2.fromOffset(30, 1)
		text.Size = UDim2.new(1, -30, 0, 0)
		row.Parent = how
	end

	local update = self:card(body, "Update", 3, 6)
	self:heading(update, "Update and send back", 1)
	label(
		update,
		"Text",
		"Import a new version the same way: the earlier import is updated in place, and your scripts stay. Export to designer saves the selected UI as a file for "
			.. tool
			.. ".",
		13,
		colors.text,
		Enum.FontWeight.Regular,
		2
	)

	local images = self:card(body, "Images", 4, 6)
	self:heading(images, "Image uploads", 1)
	label(
		images,
		"Text",
		"Images are uploaded to your account (or a group, in Settings), once each. If Studio doesn't allow it: sign in, turn on File → Beta Features → CreateAssetAsync Luau API, then restart Studio.",
		13,
		colors.text,
		Enum.FontWeight.Regular,
		2
	)

	local guide = self:card(body, "Guide", 5, 8)
	self:heading(guide, "The full guide", 1)
	label(guide, "Text", "Plugins can't open a browser: click the address, press Ctrl+C, and paste it in your browser.", 13, colors.text, Enum.FontWeight.Regular, 2)
	self.parts.Link = self:linkBox(guide, "Link", self.info.guideUrl, 3)
end

function Ui.refresh(self: Ui, part: string)
	if self.destroyed then
		return
	end
	local main: GuiObject? = self.parts.MainColumn
	if main == nil or not main:IsDescendantOf(self.root) then
		self:render()
		return
	end
	if part == "Actions" then
		self:renderPaste()
		self:renderMore()
		self:followLeave("main", self.parts.Paste, self.parts.More)
	elseif part == "Card" then
		self:renderCard()
		self:followLeave("main", self.parts.Card, self.parts.Recent)
	elseif part == "Notices" then
		self:renderNotices()
		self:followLeave("main", self.parts.Notices)
	else
		self:renderSettings()
		self:renderHelp()
		self:followLeave("settings", self.parts.SettingsBody)
		self:followLeave("help", self.parts.HelpBody)
	end
	self:prune()
end

local NEEDS_LOOK = { pasting = true, summary = true, blocked = true, done = true, error = true }

function Ui.show(self: Ui, state: State)
	local previous = self.state
	local wasBusy = previous.busy == true
	self.state = state
	local fresh = state.view ~= previous.view or state.title ~= previous.title
	self.pop = fresh
	if wasBusy ~= (state.busy == true) then
		self:refresh("Actions")
	end
	self:refresh("Card")
	self.pop = false
	local look = self.pasteLook
	if look then
		look()
	end
	if fresh and NEEDS_LOOK[state.view] and self.page ~= "main" then
		self:showPage("main")
	end
end

function Ui.setSettings(self: Ui, settingsState: SettingsState)
	self.settingsState = settingsState
	self:refresh("Settings")
end

function Ui.setNotices(self: Ui, notices: { Notice })
	self.notices = notices
	self:refresh("Notices")
end

function Ui.press(self: Ui, name: string): boolean
	for item, action in self.actions do
		if item.Name == name and shown(item, self.root, self.leaving) then
			action()
			return true
		end
	end
	return false
end

function Ui.poke(self: Ui, name: string, state: string): boolean
	for item, look in self.looks do
		if item.Name == name and shown(item, self.root, self.leaving) then
			look(state)
			return true
		end
	end
	return false
end

function Ui.texts(self: Ui): { string }
	local out = {}
	for _, item in self.root:GetDescendants() do
		if (item:IsA("TextLabel") or item:IsA("TextButton") or item:IsA("TextBox")) and item.Text ~= "" and self:built(item, "TextTransparency") < 1 and shown(item, self.root, self.leaving) then
			table.insert(out, item.Text)
		end
	end
	return out
end

function Ui.destroy(self: Ui)
	self.destroyed = true
	for _, connection in self.connections do
		connection:Disconnect()
	end
	table.clear(self.connections)
	for _, tween in self.loops do
		tween:Cancel()
	end
	table.clear(self.loops)
	table.clear(self.actions)
	table.clear(self.looks)
	table.clear(self.resting)
	table.clear(self.leaving)
	self.root:Destroy()
end

return Ui
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX40AD89746490CF17509C4A1610906F67">
			<Properties>
				<string name="Name">Updates</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local HttpService = game:GetService("HttpService")

local Brand = require(script.Parent.Brand)
local Settings = require(script.Parent.Settings)
local Version = require(script.Parent.Version)

local Updates = {}

Updates.EVERY = 24 * 60 * 60
Updates.MAX_BYTES = 16 * 1024
Updates.MAX_LINE = 200

export type Release = Settings.Release

function Updates.siteUrl(value: any): string
	if
		type(value) == "string"
		and #value <= 300
		and string.sub(value, 1, #Brand.siteOrigin + 1) == Brand.siteOrigin .. "/"
		and not string.find(value, "[%s%c]")
	then
		return value
	end
	return Brand.guideUrl
end

function Updates.line(value: any): string?
	if type(value) ~= "string" or #value > 4 * Updates.MAX_LINE or utf8.len(value) == nil then
		return nil
	end
	local text = string.gsub(value, "%c", " ")
	text = string.gsub(text, "%s+", " ")
	text = string.match(text, "^%s*(.-)%s*$") or ""
	if text == "" then
		return nil
	end
	local length = utf8.len(text) or 0
	if length > Updates.MAX_LINE then
		local cut = utf8.offset(text, Updates.MAX_LINE) or (#text + 1)
		text = (string.match(string.sub(text, 1, cut - 1), "^(.-)%s*$") or "") .. "…"
	end
	return text
end

function Updates.release(value: any): Release?
	if type(value) ~= "table" then
		return nil
	end
	local release = Version.release(value.release)
	if Version.parse(value.version) == nil or release == nil then
		return nil
	end
	return {
		version = value.version,
		release = release,
		notes = Updates.line(value.notes),
		url = Updates.siteUrl(value.guide or value.url),
	}
end

function Updates.read(text: any): Release?
	if type(text) ~= "string" or #text > Updates.MAX_BYTES then
		return nil
	end
	local ok, data = pcall(HttpService.JSONDecode, HttpService, text)
	if not ok or type(data) ~= "table" or data.format ~= "nighty-version" then
		return nil
	end
	return Updates.release(data)
end

function Updates.due(state: Settings.UpdateState, now: number): boolean
	if state.off then
		return false
	end
	local at = state.checkedAt
	return at == nil or now - at >= Updates.EVERY or now < at
end

function Updates.shown(state: Settings.UpdateState, pasted: Release?, own: number): Release?
	local best = if state.off then nil else Updates.release(state.newest)
	if pasted and (best == nil or pasted.release > best.release) then
		best = pasted
	end
	if best == nil or best.release <= own then
		return nil
	end
	local later = state.later
	if later and best.release <= later then
		return nil
	end
	return best
end

function Updates.check(settings: Settings.Settings, fetch: (url: string) -> string, now: number): Release?
	local before = settings:updates()
	if not Updates.due(before, now) then
		return nil
	end
	local looked = table.clone(before)
	looked.checkedAt = now
	settings:setUpdates(looked)
	local ok, text = pcall(fetch, Brand.versionUrl)
	local found = if ok then Updates.read(text) else nil
	if found == nil then
		return nil
	end
	local state = table.clone(settings:updates())
	state.newest = found
	settings:setUpdates(state)
	return found
end

return Updates
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX180566628B58473B4C3B5F64C6040631">
			<Properties>
				<string name="Name">Uploader</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local AssetService = game:GetService("AssetService")
local StudioService = game:GetService("StudioService")

local Images = require(script.Parent.Images)
local Png = require(script.Parent.Png)

local Uploader = {}
Uploader.__index = Uploader

Uploader.HELP = {
	"Make sure you're signed in to Studio (top right).",
	'Open File → Beta Features and turn on "CreateAssetAsync Luau API" (if it is in the list), then restart Studio.',
	"If Studio asked whether the plugin may upload and you said no, allow it under Plugins → Manage Plugins, then import again.",
	"Uploading for a group? You need permission to create assets in that group.",
}

function Uploader.userId(): number
	local ok, id = pcall(function()
		return StudioService:GetUserId()
	end)
	return if ok and type(id) == "number" then id else 0
end

function Uploader.new(): Images.Uploader
	return setmetatable({}, Uploader) :: any
end

function Uploader.check(_self: any): (boolean, string?)
	local ok, method = pcall(function()
		return (AssetService :: any).CreateAssetAsync
	end)
	if not ok or type(method) ~= "function" then
		return false, "This version of Studio doesn't let plugins upload images."
	end
	if Uploader.userId() == 0 then
		return false, "Sign in to Studio to upload images."
	end
	return true, nil
end

local function mentions(message: string, words: { string }): boolean
	local lower = string.lower(message)
	for _, word in words do
		if string.find(lower, word, 1, true) then
			return true
		end
	end
	return false
end

local LIMIT_WORDS = { "too many", "rate limit", "ratelimit", "throttl", "limit exceeded" }
local NETWORK_WORDS = { "timeout", "timed out", "http", "network", "internal server", "service unavailable" }

local function fromError(message: string): Images.UploadError
	if mentions(message, LIMIT_WORDS) then
		return { kind = "limit", message = "Roblox is busy with uploads right now." }
	end
	if mentions(message, NETWORK_WORDS) then
		return { kind = "failed", message = "Roblox didn't answer (" .. message .. ")." }
	end
	return { kind = "blocked", message = "Studio didn't let the plugin upload images (" .. message .. ")." }
end

local function fromResult(message: string): Images.UploadError
	if mentions(message, LIMIT_WORDS) then
		return { kind = "limit", message = "Roblox is busy with uploads right now." }
	end
	return { kind = "failed", message = message }
end

function Uploader.upload(_self: any, image: Png.Image, name: string, description: string, creator: Images.Creator): (number?, Images.UploadError?)
	local size = Vector2.new(image.width, image.height)
	local created, editable = pcall(function()
		return AssetService:CreateEditableImage({ Size = size })
	end)
	if not created or editable == nil then
		return nil,
			{
				kind = "blocked",
				message = "Studio didn't let the plugin prepare images ("
					.. tostring(editable or "no memory left for images")
					.. "). If this place has Game Settings > Security > Allow Mesh / Image APIs, turn it on and import again.",
			}
	end
	local written, writeError = pcall(function()
		editable:WritePixelsBuffer(Vector2.zero, size, image.pixels)
	end)
	if not written then
		editable:Destroy()
		return nil, { kind = "failed", message = "The image could not be prepared (" .. tostring(writeError) .. ")." }
	end
	local parameters: { [string]: any } = {
		Name = name,
		Description = description,
		CreatorId = creator.id,
		CreatorType = if creator.kind == "group" then Enum.AssetCreatorType.Group else Enum.AssetCreatorType.User,
	}
	local called, result, idOrMessage = pcall(function()
		return AssetService:CreateAssetAsync(editable, Enum.AssetType.Image, parameters)
	end)
	editable:Destroy()
	if not called then
		return nil, fromError(tostring(result))
	end
	if result == Enum.CreateAssetResult.Success then
		local id = tonumber(idOrMessage)
		if id and id > 0 then
			return id, nil
		end
		return nil, { kind = "failed", message = "Roblox didn't send back the new image's id." }
	elseif result == Enum.CreateAssetResult.PermissionDenied then
		return nil,
			{
				kind = "blocked",
				message = if creator.kind == "group"
					then "Roblox says you can't create assets in group " .. creator.id .. "."
					else "Roblox says this account can't upload images right now.",
			}
	end
	return nil, fromResult(tostring(idOrMessage or result))
end

return Uploader
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX50A60AB4987B798443B744524CC464F8">
			<Properties>
				<string name="Name">Version</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local Version = {}

function Version.parse(value: any): { number }?
	if type(value) ~= "string" or #value > 32 then
		return nil
	end
	local major, minor, patch = string.match(value, "^(%d+)%.(%d+)%.(%d+)$")
	if major == nil or minor == nil or patch == nil then
		return nil
	end
	return { tonumber(major) :: number, tonumber(minor) :: number, tonumber(patch) :: number }
end

function Version.newer(a: any, b: any): boolean
	local x, y = Version.parse(a), Version.parse(b)
	if x == nil or y == nil then
		return false
	end
	for index = 1, 3 do
		if x[index] ~= y[index] then
			return x[index] > y[index]
		end
	end
	return false
end

function Version.release(value: any): number?
	if type(value) ~= "number" or value ~= value or value < 1 or value > 1e6 or math.floor(value) ~= value then
		return nil
	end
	return value
end

return Version
]]></ProtectedString>
			</Properties>
		</Item>
		<Item class="ModuleScript" referent="RBX776088534F612C207CD557BEFAC0283D">
			<Properties>
				<string name="Name">Zip</string>
				<ProtectedString name="Source"><![CDATA[-- Nighty · Copyright (c) 2026 NightyCode. All rights reserved. Terms in the Nighty script.
--!strict

local Brand = require(script.Parent.Brand)
local Checksum = require(script.Parent.Checksum)
local Errors = require(script.Parent.Errors)
local Inflate = require(script.Parent.Inflate)

local readu16, readu32 = buffer.readu16, buffer.readu32

export type Entry = {
	name: string,
	method: number,
	flags: number,
	crc: number,
	compressedSize: number,
	size: number,
	headerOffset: number,
}

export type Archive = {
	data: buffer,
	entries: { Entry },
	byName: { [string]: Entry },
}

local MAX_ENTRIES = 20000
local MAX_FILE_SIZE = 256 * 1024 * 1024
local NOT_ZIP = "This isn't a design file. Pick a ." .. Brand.bundleExtension .. " file exported from " .. Brand.productName .. "."
local DAMAGED = "The file is damaged and can't be read. Export it again from " .. Brand.productName .. "."

local Zip = {}

function Zip.open(data: buffer): Archive
	local size = buffer.len(data)
	if size < 22 then
		Errors.fail(NOT_ZIP, "zip: too short")
	end
	local eocd = -1
	for pos = size - 22, math.max(0, size - 22 - 65535), -1 do
		if readu32(data, pos) == 0x06054b50 then
			eocd = pos
			break
		end
	end
	if eocd < 0 then
		Errors.fail(NOT_ZIP, "zip: no end record")
	end
	local count = readu16(data, eocd + 10)
	local directorySize = readu32(data, eocd + 12)
	local directoryOffset = readu32(data, eocd + 16)
	if readu16(data, eocd + 4) ~= 0 or readu16(data, eocd + 6) ~= 0 or readu16(data, eocd + 8) ~= count then
		Errors.fail(DAMAGED, "zip: split archives are not supported")
	end
	if count == 0xFFFF or directorySize == 0xFFFFFFFF or directoryOffset == 0xFFFFFFFF then
		Errors.fail("This file is too big for the plugin to read.", "zip: ZIP64")
	end
	if count > MAX_ENTRIES then
		Errors.fail("This file holds too many files for the plugin to read.", "zip: " .. count .. " entries")
	end
	if directoryOffset + directorySize > eocd then
		Errors.fail(DAMAGED, "zip: central directory out of range")
	end

	local archive: Archive = { data = data, entries = {}, byName = {} }
	local pos = directoryOffset
	for _ = 1, count do
		if pos + 46 > eocd or readu32(data, pos) ~= 0x02014b50 then
			Errors.fail(DAMAGED, "zip: bad central directory entry")
		end
		local nameLength = readu16(data, pos + 28)
		local extraLength = readu16(data, pos + 30)
		local commentLength = readu16(data, pos + 32)
		if pos + 46 + nameLength + extraLength + commentLength > eocd then
			Errors.fail(DAMAGED, "zip: entry runs past the directory")
		end
		local entry: Entry = {
			name = (string.gsub(buffer.readstring(data, pos + 46, nameLength), "\\", "/")),
			flags = readu16(data, pos + 8),
			method = readu16(data, pos + 10),
			crc = readu32(data, pos + 16),
			compressedSize = readu32(data, pos + 20),
			size = readu32(data, pos + 24),
			headerOffset = readu32(data, pos + 42),
		}
		pos += 46 + nameLength + extraLength + commentLength
		if string.sub(entry.name, -1) ~= "/" and archive.byName[entry.name] == nil then
			table.insert(archive.entries, entry)
			archive.byName[entry.name] = entry
		end
	end
	return archive
end

function Zip.read(archive: Archive, entry: Entry): buffer
	local data = archive.data
	local size = buffer.len(data)
	if bit32.band(entry.flags, 1) ~= 0 then
		Errors.fail("This file is password-protected. Export it again from " .. Brand.productName .. ".", "zip: encrypted entry")
	end
	if entry.size > MAX_FILE_SIZE then
		Errors.fail("A file inside the design file is too big for the plugin to read.", "zip: " .. entry.name .. " too big")
	end
	local header = entry.headerOffset
	if header + 30 > size or readu32(data, header) ~= 0x04034b50 then
		Errors.fail(DAMAGED, "zip: bad local header for " .. entry.name)
	end
	local start = header + 30 + readu16(data, header + 26) + readu16(data, header + 28)
	if start + entry.compressedSize > size then
		Errors.fail(DAMAGED, "zip: data of " .. entry.name .. " runs past the end")
	end
	local out: buffer
	if entry.method == 0 then
		if entry.compressedSize ~= entry.size then
			Errors.fail(DAMAGED, "zip: stored sizes differ for " .. entry.name)
		end
		out = buffer.create(entry.size)
		buffer.copy(out, 0, data, start, entry.size)
	elseif entry.method == 8 then
		out = Inflate.raw(data, start, entry.compressedSize, entry.size)
	else
		Errors.fail(
			"This file uses a kind of compression " .. Brand.pluginName .. " can't read. Export it again from " .. Brand.productName .. ".",
			"zip: method " .. entry.method .. " for " .. entry.name
		)
	end
	if Checksum.crc32(out, 0, entry.size) ~= entry.crc then
		Errors.fail(DAMAGED, "zip: CRC mismatch for " .. entry.name)
	end
	return out
end

function Zip.find(archive: Archive, name: string): Entry?
	return archive.byName[name]
end

return Zip
]]></ProtectedString>
			</Properties>
		</Item>
	</Item>
</roblox>
