changeset 0:8f4df159f06b

start public repo
author Franklin Schmidt <fschmidt@gmail.com>
date Fri, 11 Jul 2025 20:57:49 -0600
parents
children 2776f06236b4
files .hg_archival.txt .hgignore backup/backup-local.sh backup/backup-prod.sh backup/backup-test.sh doc/backup.txt push-local.sh push-prod.sh push-test.sh redir/push.sh redir/serve.sh redir/src/index.html.luan redir/src/init.luan serve.sh src/account.html.luan src/add_link.js.luan src/add_pic.js.luan src/admin.css src/admin.js src/analytics.html.luan src/app.html.luan src/cancel_edit_icons.js.luan src/cancel_edit_link.js.luan src/change_email.html.luan src/change_email.js.luan src/change_email2.html.luan src/change_pic.js.luan src/dad.css src/dad.js src/delete_link.js.luan src/delete_pic.js.luan src/delete_user.js.luan src/edit_icons.js.luan src/edit_link.js.luan src/error_log.js.luan src/facebook.js.luan src/forgot.html.luan src/forgot.js.luan src/forgot2.html.luan src/help.html.luan src/images/1.gif src/images/bag.jpg src/images/close.svg src/images/drag_indicator.svg src/images/favicon.png src/images/help/burger.jpg src/images/help/jordan.jpg src/images/help/mid.jpg src/images/help/register.jpg src/images/help/themepage.jpg src/images/home/01.png src/images/home/02.png src/images/home/03.png src/images/home/04.png src/images/home/analytics.svg src/images/home/background.svg src/images/home/bio.png src/images/home/desktop.svg src/images/home/hyn_x.jpeg src/images/home/i1.png src/images/home/i2.png src/images/home/i3.png src/images/home/i4.png src/images/home/i5.png src/images/home/i6.png src/images/home/ig_midsize1.png src/images/home/ins.png src/images/home/kenzie.jpeg src/images/home/mail.svg src/images/home/midsize.jpeg src/images/home/midsize1.png src/images/home/midsize2.png src/images/home/midsize3.png src/images/home/midsize4.png src/images/home/mobile.svg src/images/home/tk.png src/images/home/yt.png src/images/icons/applemusic.svg src/images/icons/cashapp.svg src/images/icons/discord.svg src/images/icons/email.svg src/images/icons/facebook.svg src/images/icons/instagram.svg src/images/icons/patreon.svg src/images/icons/paypal.svg src/images/icons/pinterest.svg src/images/icons/reddit.svg src/images/icons/snapchat.svg src/images/icons/soundcloud.svg src/images/icons/spotify.svg src/images/icons/threads.svg src/images/icons/tiktok.svg src/images/icons/twitch.svg src/images/icons/twitter.svg src/images/icons/youtube.svg src/images/ios.svg src/images/keyboard_backspace.svg src/images/logo.png src/images/model.jpg src/images/nothing.svg src/images/picture.jpg src/images/play.svg src/images/shirts.jpg src/images/small_logo.png src/images/user.png src/images/visibility.svg src/images/visibility_off.svg src/index.html.luan src/init.luan src/instagram.html.luan src/job.html.luan src/lib/Ab_test.luan src/lib/Db.luan src/lib/Facebook.luan src/lib/Icon.luan src/lib/Link.luan src/lib/Pic.luan src/lib/Reporting.luan src/lib/Shared.luan src/lib/Uploadcare.luan src/lib/User.luan src/lib/Utils.luan src/lib/main_html.luan src/links.html.luan src/log_info.js.luan src/login.html.luan src/login.js.luan src/move_icon.js.luan src/move_link.js.luan src/move_pic.js.luan src/opened.gif.luan src/pic.html.luan src/pics.html.luan src/private/Config_sample.luan src/private/reports/analytics.html.luan src/private/reports/delete_user.js.luan src/private/reports/registered.txt.luan src/private/reports/registers.html.luan src/private/reports/save_source.js.luan src/private/reports/users.html.luan src/private/reports/users.json.luan src/private/tools/email.html src/private/tools/error.html src/private/tools/lucene.html.luan src/private/tools/reporting.html.luan src/private/tools/run.luan src/private/tools/send_email.txt.luan src/private/tools/shell.html.luan src/private/tools/tools.html src/private/tools/uploadcare_gc.txt.luan src/qr_code.html.luan src/qrcode.js src/register.html.luan src/register.js.luan src/register2.html.luan src/register2.js.luan src/register3.html.luan src/report.js.luan src/reporting.js src/save_account.js.luan src/save_email.js.luan src/save_icons.js.luan src/save_link.js.luan src/save_mp.js.luan src/save_pic_title.js.luan src/site.css src/site.js src/theme.css.luan src/theme.html.luan src/theme.js.luan src/tools/animation.html src/tools/cookies.html.luan src/tools/dimensions.html src/tools/request.txt.luan src/tools/tools.css src/uploadcare/blocks.html src/uploadcare/compress.html src/uploadcare/croppr.css src/uploadcare/croppr.html src/uploadcare/croppr.js src/uploadcare/croppr2.html src/uploadcare/processing.gif src/uploadcare/test1.html src/uploadcare/test2.html.luan src/uploadcare/thumbnails.html src/uploadcare/uploadcare.css src/uploadcare/uploadcare.js src/uploadcare/uploader.html src/uploadcare/uploader2.html unsubscribe/push.sh unsubscribe/redir/push.sh unsubscribe/redir/serve.sh unsubscribe/redir/src/index.html.luan unsubscribe/redir/src/init.luan unsubscribe/serve.sh unsubscribe/src/init.luan unsubscribe/src/lib/Db.luan unsubscribe/src/lib/Shared.luan unsubscribe/src/private/tools/lucene.html.luan unsubscribe/src/private/tools/tools.css unsubscribe/src/private/tools/tools.html unsubscribe/src/private/tools/unsubscribed.html.luan unsubscribe/src/site.css unsubscribe/src/subscribe.html.luan unsubscribe/src/unsubscribe.html.luan
diffstat 205 files changed, 12078 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hg_archival.txt	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,6 @@
+repo: e9f0b8be4dca9be61782d97a65641900d83463b8
+node: e38ea59f7aa637103fe4c1e3952d0d86821d52f1
+branch: default
+latesttag: null
+latesttagdistance: 552
+changessincelatesttag: 552
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,11 @@
+syntax: glob
+
+err
+.DS_Store
+mine/
+local/
+rev.txt
+*.zip
+*.bck
+test/
+private/Config.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/backup/backup-local.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+luan luan:host/backup.luan lms.me.luan.software word lucene
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/backup/backup-prod.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -e
+
+PASSWORD=$(luan 'string:require("luan:Io.luan").stdout.write(require("file:../src/private/Config.luan").push_password)')
+
+luan luan:host/backup.luan linkmy.style $PASSWORD lucene
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/backup/backup-test.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -e
+
+PASSWORD=$(luan 'string:require("luan:Io.luan").stdout.write(require("file:../src/private/Config.luan").push_password)')
+
+luan luan:host/backup.luan test.linkmy.style $PASSWORD lucene
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/backup.txt	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,3 @@
+ussh administrator@mfar.nabble.com -p 14299
+
+/Volumes/External/uploadcare/backup
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/push-local.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+luan luan:host/push.luan lms.me.luan.software word src 2>&1 | tee err
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/push-prod.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -e
+
+hg identify >src/private/rev.txt
+
+PASSWORD=$(luan 'string:require("luan:Io.luan").stdout.write(require("file:src/private/Config.luan").push_password)')
+
+luan luan:host/push.luan linkmy.style $PASSWORD src 2>&1 | tee err
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/push-test.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -e
+
+hg identify >src/private/rev.txt
+
+PASSWORD=$(luan 'string:require("luan:Io.luan").stdout.write(require("file:src/private/Config.luan").push_password)')
+
+luan luan:host/push.luan test.linkmy.style $PASSWORD src 2>&1 | tee err
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/redir/push.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -e
+
+PASSWORD=$(luan 'string:require("luan:Io.luan").stdout.write(require("file:../src/private/Config.luan").push_password)')
+
+luan luan:host/push.luan www.linkmystyle.com $PASSWORD src 2>&1 | tee err
+luan luan:host/push.luan linkmystyle.com $PASSWORD src 2>&1 | tee -a err
+luan luan:host/push.luan www.linkmy.style $PASSWORD src 2>&1 | tee -a err
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/redir/serve.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+luan luan:http/serve.luan src 2>&1 | tee err
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/redir/src/index.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,3 @@
+local Http = require "luan:http/Http.luan"
+
+return Http.not_found_handler
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/redir/src/init.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,14 @@
+local Http = require "luan:http/Http.luan"
+local Hosted = require "luan:host/Hosted.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "init"
+
+
+Hosted.set_https and Hosted.set_https(Http.domain~=nil)
+
+function Http.not_found_handler()
+	Http.response.send_redirect("https://linkmy.style"..Http.request.raw_path)
+	return true
+end
+
+return true
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/serve.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+luan luan:http/serve.luan src 8080 $(whoami) 2>&1 | tee err
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/account.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,250 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local body_header = Shared.body_header or error()
+local footer = Shared.footer or error()
+local show_user_icons = Shared.show_user_icons or error()
+local password_input = Shared.password_input or error()
+local User = require "site:/lib/User.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "account.html"
+
+
+return function()
+	local user = User.current_required()
+	if user==nil then return end
+	local title = user.title
+	local bio = user.bio
+	local username = user.name
+	local password = user.password
+	local email = user.email
+	local mp_id = user.mp_id
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+			h1 {
+				text-align: center;
+			}
+			div[body] {
+				max-width: 600px;
+				margin-left: auto;
+				margin-right: auto;
+			}
+			@media (max-width: 700px) {
+				div[body] {
+					max-width: 90%;
+				}
+			}
+			div[body] > * {
+				margin-bottom: 64px;
+			}
+			h2 {
+				margin-top: 20px;
+				margin-bottom: 20px !important;
+			}
+			label {
+				display: block;
+			}
+			div[field] {
+				margin-bottom: 10px;
+			}
+			button[delete1] {
+				background-color: red;
+			}
+			button[delete1]:hover,
+			button[delete2]:hover {
+				background-color: #7F1B1B;
+			}
+			div[delete2] {
+				display: none;
+			}
+
+			div[pic] {
+				display: flex;
+				margin-bottom: 20px;
+			}
+			div[pic] img {
+				width: 100px;
+				height: 100px;
+				object-fit: cover;
+				border-radius: 50%;
+				background-color: grey;
+				flex-shrink: 0;
+			}
+			div[pic] div {
+				width: 100%;
+				display: flex;
+				flex-direction: column;
+				align-items: center;
+				justify-content: space-around;
+			}
+			div[pic] button {
+				width: 90%;
+			}
+
+			div[icons] div[list] {
+				display: flex;
+				gap: 5px;
+				flex-wrap: wrap;
+				margin-bottom: 8px;
+			}
+			span[icon] {
+				border: 1px solid;
+				display: flex;
+				padding-right: 4px;
+			}
+			span[icon] img {
+				display: block;
+				height: 40px;
+			}
+			span[icon] > img {
+				opacity: 0.3;
+				touch-action: none;
+			}
+		</style>
+		<script>
+			'use strict';
+
+			function delete1() {
+				let delete2 = document.querySelector('div[delete2]');
+				delete2.style.display = 'block';
+				delete2.scrollIntoViewIfNeeded(false);
+			}
+			function undelete1() {
+				document.querySelector('div[delete2]').style.display = 'none';
+			}
+			function delete2() {
+				ajax( '/delete_user.js' );
+			}
+
+			function removePic() {
+				let img = document.querySelector('div[pic] img');
+				img.src = '/images/nothing.svg';
+				let pic_uuid = document.querySelector('input[name="pic_uuid"]');
+				pic_uuid.value = 'remove';
+			}
+			function uploaded(uuid,filename) {
+				document.querySelector('input[name="pic_uuid"]').value = uuid;
+				document.querySelector('input[name="pic_filename"]').value = filename;
+				let img = document.querySelector('div[pic] img');
+				img.src = uploadcareUrl(uuid);
+			}
+			function startUpload() {
+				uploadcare.cropprOptions = {aspectRatio: 1};
+				uploadcare.upload(uploaded);
+			}
+
+			dad.onDropped = function(event) {
+				let dragging = event.original;
+				if( iDragging === indexOf(dragging.parentNode.querySelectorAll(dropSelector),dragging) )
+					return;
+				let iconId = dragging.getAttribute('icon');
+				let prev = dragging.previousElementSibling;
+				let prevId = prev && prev.getAttribute('icon');
+				ajax( '/move_icon.js?icon='+iconId+'&prev='+prevId );
+			};
+			dad.whatToDrag = function(draggable) {
+				return draggable.parentNode;
+			};
+			function dragInit() {
+				dropSelector = 'span[icon]';
+				let items = document.querySelectorAll('span[icon] > img');
+				for( let i=0; i<items.length; i++ ) {
+					let item = items[i];
+					dad.setDraggable(item);
+					dad.setDropzone(item.parentNode);
+				}
+			}
+		</script>
+	</head>
+	<body>
+	<div full>
+<%		body_header() %>
+		<h1>My Account</h1>
+		<div body>
+			<form onsubmit="ajaxForm('/save_account.js',this)" action="javascript:">
+				<h2>My information</h2>
+				<div pic>
+					<img src="<%= user.get_pic_url() or "/images/nothing.svg" %>">
+					<div>
+						<input type=hidden name="pic_uuid">
+						<input type=hidden name="pic_filename">
+						<button type=button big onclick="startUpload()">Pick an image</button>
+						<button type=button big onclick="removePic()">Remove</button>
+					</div>
+				</div>
+				<label>Profile Title</label>
+				<div field>
+					<input type=text name=title value="<%= html_encode(title or username) %>">
+				</div>
+				<label>Profile Bio</label>
+				<div field>
+					<textarea name=bio><%= html_encode(bio or "") %></textarea>
+				</div>
+				<label>Username</label>
+				<div field>
+					<input type=text required name=username placeholder="Username" value="<%= username %>">
+					<div error=username></div>
+				</div>
+				<label>Password</label>
+				<div field>
+<%					password_input(password) %>
+				</div>
+				<button type=submit big>Save</button>
+				<div error=success></div>
+			</form>
+
+<%
+%>
+			<h2 icons>Social icons</h2>
+			<div icons>
+<%				show_user_icons(user) %>
+			</div>
+
+			<form onsubmit="ajaxForm('/save_email.js',this)" action="javascript:">
+				<h2>My email</h2>
+				<label>Email</label>
+				<div field>
+					<input type=email required name=email placeholder="Email" value="<%= email %>">
+					<div error=email></div>
+				</div>
+				<button type=submit big>Change</button>
+			</form>
+
+			<form onsubmit="ajaxForm('/save_mp.js',this)" action="javascript:">
+				<h2>Custom analytics</h2>
+				<label>Mixpanel Project Token</label>
+				<div field>
+					<input type=text name=mp_id value="<%= html_encode(mp_id or "") %>">
+				</div>
+				<button type=submit big>Save</button>
+				<div error=success></div>
+			</form>
+
+			<div>
+				<h2>Delete account</h2>
+				<button type=button delete1 big onclick="delete1()">Delete account</button>
+				<div delete2>
+					<p>Are you sure that you want to delete this account?</p>
+					<button type=button delete2 small onclick="delete2()">Yes</button>
+					<button type=button small onclick="undelete1()">No</button>
+				</div>
+			</div>
+		</div>
+<%		footer() %>
+	</div>
+	</body>
+	<script> dragInit(); </script>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/add_link.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,50 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+local Link = require "site:/lib/Link.luan"
+local get_owner_links = Link.get_owner_links or error()
+local Shared = require "site:/lib/Shared.luan"
+local show_editable_link = Shared.show_editable_link or error()
+local User = require "site:/lib/User.luan"
+local Pic = require "site:/lib/Pic.luan"
+
+
+return function()
+	local user = User.current() or error()
+	local url = Http.request.parameters.url or error()
+	local title = Http.request.parameters.title or error()
+	local owner = user
+	local pic = Http.request.parameters.pic
+	if pic ~= nil then
+		pic = Pic.get_by_id(pic) or error()
+		pic.user_id == user.id or error()
+		owner = pic
+	end
+	local link
+	run_in_transaction( function()
+		local links = get_owner_links(user.id)
+		link = Link.new{
+			url = url
+			title = title
+			owner_id = owner.id
+			user_id = user.id
+			order = #links > 0 and links[1].order - 1 or 0
+		}
+		link.save()
+	end )
+	local html = ` show_editable_link(link) `
+	Io.stdout = Http.response.text_writer()
+%>
+	let startDiv = document.querySelector('div[start]');
+	startDiv.insertAdjacentHTML( 'afterend', <%= json_string(html) %> );
+	clearAddForm();
+	dragInit();
+	mixpanel.ours.track( 'Add Link' );
+	fbTrack( 'track', 'AddToCart' );
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/add_pic.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,36 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+local Pic = require "site:/lib/Pic.luan"
+local get_user_pics = Pic.get_user_pics or error()
+local User = require "site:/lib/User.luan"
+
+
+return function()
+	local user = User.current() or error()
+	local title = Http.request.parameters.title or error()
+	local is_hidden = Http.request.parameters.visible == nil
+	local uuid = Http.request.parameters.uuid or error()
+	local filename = Http.request.parameters.filename or error()
+	local pic
+	run_in_transaction( function()
+		local pics = get_user_pics(user.id)
+		pic = Pic.new{
+			uuid = uuid
+			filename = filename
+			user_id = user.id
+			title = title
+			is_hidden = is_hidden
+			order = #pics > 0 and pics[1].order - 1 or 0
+		}
+		pic.save()
+	end )
+	Io.stdout = Http.response.text_writer()
+%>
+	mixpanel.ours.track( 'Add Pic', null, {send_immediately:true} );
+	location = '/links.html?saved&pic=<%=pic.id%>';
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/admin.css	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,302 @@
+a {
+	color: #472C7D;
+}
+
+a[button] {
+	display: inline-block;
+	text-decoration: none;
+}
+
+button,
+[onclick],
+[clickable],
+input[type="radio"],
+input[type="submit"] {
+	cursor: pointer;
+}
+
+[pulldown] {
+	position: relative;
+}
+[pulldown_menu] {
+	display: none;
+	z-index: 20;
+	position: absolute;
+	top: 50px;
+	border: 1px solid #DDDDDD;
+	text-align: left;
+	background-color: #ffffff;
+	border-radius: 12px;
+}
+[pulldown_menu] a {
+	display: block;
+	color: black;
+	padding-left: 32px;
+	padding-right: 32px;
+	white-space: nowrap;
+	text-decoration: none;
+}
+[pulldown_menu] a:hover {
+	text-decoration: underline;
+}
+[pulldown_menu] a {
+	font-size: 15px;
+	margin-top: 12px;
+	margin-bottom: 12px;
+}
+
+input[clipboard] {
+	position: absolute;
+	top: -1000px;
+}
+
+[pulldown_menu] span[copy] {
+	display: inline-block;
+	text-align: right;
+	min-width: 5em;
+}
+
+[pulldown_menu] span[copy=copied] {
+	color: lime;
+}
+
+div[header] {
+	display: flex;
+	padding: 20px 40px;
+	justify-content: space-between;
+	align-items: center;
+	background-color: #DBD5FF;
+}
+div[header] > a[left] {
+	font-size: 28px;
+	text-decoration: none;
+	color: black;
+}
+img[logo] {
+	height: 50px;
+	display: block;
+}
+@media (min-width: 757px) {
+	img[logo="small"] {
+		display: none;
+	}
+}
+@media (max-width: 756px) {
+	img[logo="big"] {
+		display: none;
+	}
+}
+div[header] [pulldown_menu] {
+	right: 0;
+}
+div[header] > span[right] img {
+	width: 50px;
+	height: 50px;
+	object-fit: cover;
+	border-radius: 50%;
+}
+div[header] > span[right][login] {
+	display: flex;
+	gap: 10px;
+}
+div[header] > span[right][login] a[button] {
+	padding: 18px 26px 20px 26px;
+}
+div[header] > span[right][login] a[login] {
+	color: black;
+	background-color: #EAEAEA;
+}
+div[header] > span[right][login] a[login]:hover {
+	background-color: #D1D1D1;
+}
+div[header] > span[right][login] a[register] {
+	color: white;
+	background-color: #4E4293;
+}
+div[header] > span[right][login] a[register]:hover {
+	background-color: #272A3B;
+}
+
+[page] {
+	padding-left: 40px;
+	padding-right: 40px;
+}
+[page] > * {
+	max-width: 600px;
+	margin-left: auto;
+	margin-right: auto;
+}
+[page] a[header] {
+	display: block;
+	color: black;
+	text-decoration: none;
+	padding-top: 20px;
+	margin-bottom: 100px;
+	font-size: 28px;
+}
+[page] h1 {
+	color: #4E4293;
+}
+[page] h1 + p {
+	color: #9E9E9E;
+}
+input[type="url"],
+input[type="password"],
+input[type="email"],
+input[type="text"] {
+	display: block;
+	width: 100%;
+	border: none;
+	background-color: #E0E0E0;
+	padding: 12px;
+	font-size: 16px;
+}
+textarea {
+	display: block;
+	width: 100%;
+	background-color: #E0E0E0;
+	padding: 12px;
+	font-size: 16px;
+	font-family: inherit;
+}
+[page] input[type="url"]:not(:first-of-type),
+[page] input[type="password"]:not(:first-of-type),
+[page] div[password],
+[page] input[type="email"]:not(:first-of-type),
+[page] input[type="text"]:not(:first-of-type) {
+	margin-top: 32px;
+}
+
+[page],
+[full] {
+	min-height: 100vh;
+	padding-bottom: 77px;
+	position: relative;
+}
+div[footer] {
+	padding: 20px 40px;
+	background-color: #DBD5FF;
+	color: #4E4293;
+	position: absolute;
+	bottom: 0;
+	width: 100%;
+	display: flex;
+	justify-content: space-between;
+}
+
+div[footer] img {
+	height: 40px;
+	display: inline-block;
+}
+div[footer] img[ios] {
+	padding-top: 4px;
+	padding-bottom: 4px;
+}
+[page] > div[footer] {
+	padding: 20px 0;
+	background-color: white;
+	position: initial;
+}
+
+div[error] {
+	color: firebrick;
+}
+div[error][flash] {
+	color: red;
+}
+div[error="success"] {
+	color: green;
+}
+div[error="success"][flash] {
+	color: lime;
+}
+
+div[password] {
+	position: relative;
+}
+div[password] input {
+	padding-right: 40px;
+}
+div[password] img {
+	position: absolute;
+	top: 11px;
+	right: 10px;
+	height: 22px;
+	opacity: .6;
+	cursor: pointer;
+}
+div[password] img[show] {
+	display: block;
+}
+div[password] img[hide] {
+	display: none;
+}
+
+button[big],
+a[button][big] {
+	display: block;
+	width: 100%;
+	border-radius: 12px / 50%;
+	color: white;
+	background-color: #9181EE;
+	text-align: center;
+	padding: 12px;
+	border: none;
+	font: inherit;
+}
+[page] button[big],
+[page] a[button][big] {
+	margin-top: 64px;
+}
+button[big]:hover,
+a[button][big]:hover {
+	background-color: #4E4293;
+}
+@media (min-width: 800px) {
+	body:has([right_of_page]) [page] {
+		width: 60%;
+	}
+	body:has([right_of_page]) [right_of_page] {
+		display: block;
+		position: fixed;
+		top: 0;
+		right: 0;
+		bottom: 0;
+		left: 60%;
+		background-size: cover;
+		background-repeat: no-repeat;
+		background-position: center;
+	}
+}
+
+button[small] {
+	border-radius: 12px / 50%;
+	color: white;
+	background-color: #151721;
+	text-align: center;
+	padding-left: 12px;
+	padding-right: 12px;
+	padding-top: 6px;
+	padding-bottom: 6px;
+	border: none;
+	font: inherit;
+}
+button[small]:hover {
+	background-color: #272A3B;
+}
+
+input[type="file"] {
+	display: none;
+}
+
+p[top] {
+	max-width: 600px;
+	margin-left: auto;
+	margin-right: auto;
+	color: #808080;
+}
+@media (max-width: 700px) {
+	p[top] {
+		max-width: 90%;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/admin.js	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,200 @@
+'use strict';
+
+let currentPulldown = null;
+let newPulldown = null;
+
+function clickMenu(clicked,display) {
+	//console.log("clickMenu");
+	let pulldown = clicked.parentNode.querySelector('[pulldown_menu]');
+	if( pulldown !== currentPulldown ) {
+		pulldown.style.display = display || "block";
+		newPulldown = pulldown;
+		window.onclick = function() {
+			//console.log("window.onclick");
+			if( currentPulldown ) {
+				currentPulldown.style.display = "none";
+				if( !newPulldown )
+					window.onclick = null;
+			}
+			currentPulldown = newPulldown;
+			newPulldown = null;
+		};
+		pulldown.scrollIntoViewIfNeeded(false);
+	}
+}
+
+function stopPropagation(event) {
+	event.stopPropagation();
+}
+
+window.addEventListener( 'load', function() {
+	for( let pulldown of document.querySelectorAll('[pulldown_menu]') ) {
+		pulldown.onclick = stopPropagation;
+	}
+} );
+
+function copyLink() {
+	// avoid the paranoid security nonsense with navigator.clipboard
+	let input = document.querySelector('input[clipboard]');
+	input.select();
+	document.execCommand('copy');
+	input.blur();
+
+	let span = document.querySelector('[pulldown_menu] span[copy]');
+	span.textContent = 'Copied!';
+	span.setAttribute('copy','copied');
+	setTimeout(function(){
+		span.textContent = 'Copy';
+		span.setAttribute('copy','');
+	} ,1000);
+}
+
+function logout() {
+	document.cookie = 'user=; Max-Age=0; path=/;';
+	document.cookie = 'password=; Max-Age=0; path=/;';
+	location = '/';
+}
+
+
+function uploadcareUrl(uuid) {
+	return "https://ucarecdn.com/" + uuid + "/-/quality/smart/";
+}
+
+if( typeof(uploadcare) !== 'undefined' ) {
+	uploadcare.publicKey = window.uploadcarePubKey;
+	uploadcare.imagesOnly = true;
+	uploadcare.maxFileSize = 10000000;
+	uploadcare.onError = function(status,text) {
+		let err = 'upload failed: ' + status;
+		if( text ) {
+			err += '\n' + text;
+		}
+		console.log(err);
+		ajax( '/error_log.js', 'err='+encodeURIComponent(err) );
+	};
+}
+
+
+let dropSelector, iDragging;
+
+function indexOf(a,el) {
+	for( let i=0; i<a.length; i++ ) {
+		if( a[i] === el )
+			return i;
+	}
+	return -1;
+}
+
+if( typeof(dad) !== 'undefined' ) {
+	dad.onStart = function(event) {
+		let dragging = event.original;
+		iDragging = indexOf(dragging.parentNode.querySelectorAll(dropSelector),dragging);
+	}
+
+	dad.onEnter = function(event) {
+		let dropzone = event.dropzone
+		let original = event.original
+		let items = document.querySelectorAll(dropSelector);
+		let iDropzone = indexOf(items,dropzone);
+		let iOriginal = indexOf(items,original);
+		let where = iDropzone < iOriginal ? 'beforebegin' : 'afterend';
+		dropzone.insertAdjacentElement(where,original);
+	};
+}
+
+function date(time) {
+	document.write(new Date(time).toLocaleDateString());
+}
+
+function ajaxForm(url,form) {
+	let post = '';
+	for( let i=0; i<form.length; i++ ) {
+		let input = form[i];
+		let name = input.name;
+		if( name === '' )
+			continue;
+		let type = input.type;
+		if( (type==='radio' || type==='checkbox') && !input.checked )
+			continue;
+		post += name + '=' + encodeURIComponent(input.value) + '&';
+	}
+	ajax(url,post,{form:form});
+}
+
+function clearErrors(form) {
+	let divs = form.querySelectorAll('div[error]');
+	for( let i=0; i<divs.length; i++ ) {
+		divs[i].textContent = '';
+	}
+}
+
+function showError(form,field,message) {
+	clearErrors(form);
+	let err = form.querySelector('[error="'+field+'"]');
+	err.textContent = message;
+	err.scrollIntoViewIfNeeded(false);
+	err.setAttribute('flash','');
+	setTimeout(function(){err.removeAttribute('flash')},2000);
+}
+
+function showPassword(div) {
+	div.querySelector('img[show]').style.display = 'none';
+	div.querySelector('img[hide]').style.display = 'block';
+	let input = div.querySelector('input');
+	input.type = 'text';
+	input.focus();
+}
+function hidePassword(div) {
+	div.querySelector('img[show]').style.display = 'block';
+	div.querySelector('img[hide]').style.display = 'none';
+	let input = div.querySelector('input');
+	input.type = 'password';
+	input.focus();
+}
+
+
+function isEmpty(obj) {
+	for( let _ in obj ) {
+		return false;
+	}
+	return true;
+}
+
+function barChartHeight(rows) {
+	return 200 + 20*rows;
+}
+
+
+// Mixpanel
+if( window.UserEmail ) {
+	mixpanel.ours.identify(window.UserEmail);
+	mixpanel.ours.people.set({'$email':window.UserEmail,'$name':window.UserName});
+}
+
+
+// A/B tests
+
+function setAbTest(name,values) {
+	let props = {};
+	props[name] = cookies[name];
+	for( let i=1; i<=4; i++ ) {
+		props[name+'-null-'+i] = values[ Math.floor( Math.random() * values.length ) ];
+	}
+	mixpanel.ours.identify();
+	mixpanel.ours.people.set(props);
+}
+
+function removeAbTest(name) {
+	let a = [];
+	a.push(name);
+	for( let i=1; i<=4; i++ ) {
+		a.push(name+'-null-'+i);
+	}
+	mixpanel.ours.people.unset(a);
+}
+
+
+
+if( !location.pathname.match(/^\/private\//) ) {
+	fbTrack( 'track', 'PageView' );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/analytics.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,189 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local body_header = Shared.body_header or error()
+local footer = Shared.footer or error()
+local compressed = Shared.compressed or error()
+local User = require "site:/lib/User.luan"
+local Reporting = require "site:/lib/Reporting.luan"
+local get_data = Reporting.get_data or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "analytics_new.html"
+
+
+return function()
+	local user = User.current_required()
+	if user==nil then return end
+	local user_name = user.name
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+			h1 {
+				text-align: center;
+				margin-bottom: 0;
+			}
+			p[top] {
+				text-align: center;
+			}
+			div[report] {
+				max-width: 600px;
+				margin-left: auto;
+				margin-right: auto;
+				margin-top: 20px;
+				margin-bottom: 20px;
+			}
+			@media (max-width: 700px) {
+				div[report] {
+					max-width: 90%;
+				}
+			}
+		</style>
+		<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
+		<script>
+<%
+	do
+		local data = get_data( "+type:visit +owner:"..user_name )
+%>
+			function initTraffic() {
+				let options = {
+					chart: {
+						type: 'line'
+					},
+					series: [{
+						name: 'Visitors',
+						data: <%=json_string(data,compressed)%>,
+					}],
+					xaxis: {
+						type: 'datetime'
+					},
+					title: {
+						text: 'Traffic'
+					}
+				};
+				let div = document.querySelector('div[report="traffic"]');
+				let chart = new ApexCharts( div, options );
+				chart.render();
+			}
+<%
+	end_do
+	do
+		local data = get_data( "+type:page_view +owner:"..user_name, "value" )
+%>
+			function initPages() {
+				let data = <%=json_string(data,compressed)%>;
+				let options = {
+					chart: {
+						type: 'bar',
+						height: barChartHeight(data.length)
+					},
+					plotOptions: {
+						bar: {
+							horizontal: true
+						}
+					},
+					series: [{
+						name: 'Visitors',
+						data: data,
+					}],
+					title: {
+						text: 'Pages'
+					}
+				};
+				let div = document.querySelector('div[report="pages"]');
+				let chart = new ApexCharts( div, options );
+				chart.render();
+			}
+<%
+	end_do
+	do
+		local data = get_data( "+type:link_click +owner:"..user_name, "value" )
+%>
+			function initClicks() {
+				let data = <%=json_string(data,compressed)%>;
+				let options = {
+					chart: {
+						type: 'bar',
+						height: barChartHeight(data.length)
+					},
+					plotOptions: {
+						bar: {
+							horizontal: true
+						}
+					},
+					series: [{
+						name: 'Clicks',
+						data: data,
+					}],
+					title: {
+						text: 'Clicks by page and link'
+					}
+				};
+				let div = document.querySelector('div[report="clicks"]');
+				let chart = new ApexCharts( div, options );
+				chart.render();
+			}
+<%
+	end_do
+	do
+		local data = get_data( "+type:referrer +owner:"..user_name, "value" )
+%>
+			function initReferrers() {
+				let data = <%=json_string(data,compressed)%>;
+				let options = {
+					chart: {
+						type: 'bar',
+						height: barChartHeight(data.length)
+					},
+					plotOptions: {
+						bar: {
+							horizontal: true
+						}
+					},
+					series: [{
+						name: 'Visitors',
+						data: data,
+					}],
+					title: {
+						text: 'Referring Domains'
+					}
+				};
+				let div = document.querySelector('div[report="referrers"]');
+				let chart = new ApexCharts( div, options );
+				chart.render();
+			}
+<%
+	end_do
+%>
+			function init() {
+				initTraffic();
+				initPages();
+				initClicks();
+				initReferrers();
+			}
+		</script>
+	</head>
+	<body onload="init()">
+	<div full>
+<%		body_header() %>
+		<h1>Analytics</h1>
+		<p top>For the last 30 days</p>
+		<div report=traffic></div>
+		<div report=pages></div>
+		<div report=clicks></div>
+		<div report=referrers></div>
+<%		footer() %>
+	</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,15 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local account = require "site:/account.html.luan"
+local login = require "site:/login.html.luan"
+local User = require "site:/lib/User.luan"
+local current_user = User.current or error()
+
+
+return function()
+	if current_user() == nil then
+		login()
+	else
+		account()
+	end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cancel_edit_icons.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,20 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local show_user_icons = Shared.show_user_icons or error()
+local User = require "site:/lib/User.luan"
+
+
+return function()
+	local user = User.current() or error()
+	local html = ` show_user_icons(user) `
+	Io.stdout = Http.response.text_writer()
+%>
+	document.querySelector('div[icons]').innerHTML = <%= json_string(html) %>;
+	document.querySelector('h2[icons]').scrollIntoViewIfNeeded(false);
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cancel_edit_link.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,21 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Link = require "site:/lib/Link.luan"
+local Shared = require "site:/lib/Shared.luan"
+local show_editable_link = Shared.show_editable_link or error()
+
+
+return function()
+	local link_id = Http.request.parameters.link or error()
+	local link = Link.get_by_id(link_id)
+	local html = ` show_editable_link(link) `
+	Io.stdout = Http.response.text_writer()
+%>
+	document.querySelector('div[link="<%=link_id%>"]').outerHTML = <%= json_string(html) %>;
+	dragInit();
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/change_email.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,42 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local page_header = Shared.page_header or error()
+local footer = Shared.footer or error()
+local User = require "site:/lib/User.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "change_email.html"
+
+
+return function()
+	local user = User.current_required()
+	if user==nil then return end
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+	</head>
+	<body>
+		<form page onsubmit="ajaxForm('/change_email.js',this)" action="javascript:">
+<%			page_header() %>
+			<div>
+				<h1>Enter confirmation code</h1>
+				<p>To change your email, enter the six digit code we sent to your inbox  (<%=html_encode(user.new_email)%>).</p>
+				<input type=text required name=code placeholder="Enter 6-digit code">
+				<div error=code></div>
+				<button type=submit big>Continue</button>
+			</div>
+<%			footer() %>
+		</form>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/change_email.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,35 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local User = require "site:/lib/User.luan"
+local Shared = require "site:/lib/Shared.luan"
+local js_error = Shared.js_error or error()
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+
+
+return function()
+	local user = User.current() or error()
+	local code = Http.request.parameters.code or error()
+	local err_fld, err_msg = run_in_transaction( function()
+		user = user.reload()
+		if user.code ~= code then
+			return "code", "Incorrect code"
+		end
+		user.code = nil
+		user.email = user.new_email or error()
+		user.new_email = nil
+		user.save()
+	end )
+	Io.stdout = Http.response.text_writer()
+	if err_fld ~= nil then
+		js_error(err_fld,err_msg)
+		return
+	end
+	user.login()
+%>
+	clearErrors(context.form);
+	location = '/change_email2.html';
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/change_email2.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,37 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local page_header = Shared.page_header or error()
+local footer = Shared.footer or error()
+local User = require "site:/lib/User.luan"
+
+
+return function()
+	local user = User.current_required()
+	if user==nil then return end
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+	</head>
+	<body>
+		<div page>
+<%			page_header() %>
+			<div>
+				<h1>Your email has been changed</h1>
+				<a button big href="/">Continue to Link My Style</a>
+			</div>
+<%			footer() %>
+		</form>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/change_pic.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,30 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Pic = require "site:/lib/Pic.luan"
+local User = require "site:/lib/User.luan"
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+
+
+return function()
+	local user = User.current() or error()
+	local pic_id = Http.request.parameters.pic or error()
+	local uuid = Http.request.parameters.uuid or error()
+	local filename = Http.request.parameters.filename or error()
+	local pic = Pic.get_by_id(pic_id)
+	pic.user_id == user.id or error()
+	run_in_transaction( function()
+		pic = pic.reload()
+		pic.uuid = uuid
+		pic.filename = filename
+		pic.save()
+	end )
+	Io.stdout = Http.response.text_writer()
+%>
+	document.querySelector('div[pic] img').src = <%= json_string(pic.get_url()) %>;
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/dad.css	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,14 @@
+[dad-drag] {
+	cursor: move;
+	touch-action: none;
+}
+
+[dad-original] {
+	visibility: hidden !important;
+}
+
+[dad-dragging] {
+	position: fixed !important;
+	margin: 0 !important;
+	z-index: 100;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/dad.js	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,155 @@
+'use strict';
+
+let dad = {};
+
+{
+	// override these if needed
+	dad.whatToDrag = function(draggable) {
+		return draggable;
+	};
+	dad.onStart = function(event) {};
+	dad.onEnter = function(event) {};
+	dad.onLeave = function(event) {};
+	dad.onDrop = function(event) {};
+	dad.onDropped = function(event) {};
+
+
+	let original = null;
+	let dragging = null;
+	let dropzone = null;
+	let touchX, touchY;
+
+	function isIn(x,y,rect) {
+		return rect.x <= x && x <= rect.x+rect.width && rect.y <= y && y <= rect.y+rect.height;
+	}
+
+	function myEvent(mouseEvent) {
+		return {
+			original: original,
+			dragging: dragging,
+			dropzone: dropzone,
+			mouseEvent: mouseEvent,
+		};
+	}
+
+	function onMouseMove(event) {
+		{
+			event.preventDefault();
+			let rect = dragging.getBoundingClientRect();
+			dragging.style.left = `${rect.x+event.movementX}px`;
+			let y = rect.y + event.movementY;
+			if( y < 0 ) {
+				window.scrollBy( 0, y );
+			} else if( y + rect.height > window.innerHeight ) {
+				window.scrollBy( 0, y + rect.height - window.innerHeight );
+			}
+			dragging.style.top = `${y}px`;
+		}
+		{
+			let x = event.clientX;
+			let y = event.clientY;
+			if( !(dropzone && isIn(x,y,dropzone.getBoundingClientRect())) ) {
+				if( dropzone ) {
+					dad.onLeave(myEvent(event));
+					dropzone = null;
+				}
+				let dropzones = document.querySelectorAll('[dad-dropzone]');
+				for( let i=0; i<dropzones.length; i++ ) {
+					let dz = dropzones[i];
+					if( dz === dragging )
+						continue;
+					if( isIn(x,y,dz.getBoundingClientRect()) ) {
+						let old = dropzone;
+						dropzone = dz;
+						let f = dad.onEnter(myEvent(event));
+						if( f === false )
+							dropzone = old;
+						else
+							break;
+					}
+				}
+			}
+		}
+	}
+
+	function onTouchMove(event) {
+		let touches = event.touches;
+		if( touches.length !== 1 )
+			return;
+		let touch = touches[0];
+		let x = touch.clientX;
+		let y = touch.clientY;
+		event.clientX = x;
+		event.clientY = y;
+		event.movementX = x - touchX;
+		event.movementY = y - touchY;
+		touchX = x;
+		touchY = y;
+		onMouseMove(event);
+	}
+
+	function onMouseUp(event) {
+		dad.onDrop(myEvent(event));
+		if( dropzone ) {
+			dad.onLeave(myEvent(event));
+		}
+		original.removeAttribute('dad-original');
+		dragging.parentNode.removeChild(dragging);
+		document.removeEventListener('mousemove',onMouseMove);
+		document.removeEventListener('mouseup',onMouseUp);
+		document.removeEventListener('touchmove',onTouchMove);
+		document.removeEventListener('touchend',onMouseUp);
+		original.scrollIntoViewIfNeeded(false);
+		let droppedEvent = {
+			original: original,
+		};
+		original = null;
+		dragging = null;
+		dropzone = null;
+		dad.onDropped(droppedEvent);
+	}
+
+	function start(event) {
+		original = dad.whatToDrag(event.target);
+		dragging = original.cloneNode(true);
+		original.setAttribute('dad-original','');
+		dragging.setAttribute('dad-dragging','');
+		let rect = original.getBoundingClientRect();
+		dragging.style.left = `${rect.x}px`;
+		dragging.style.top = `${rect.y}px`;
+		dragging.style.width = `${rect.width}px`;
+		dragging.style.height = `${rect.height}px`;
+		original.parentNode.appendChild(dragging);
+		dad.onStart(myEvent(event));
+	}
+
+	function onMouseDown(event) {
+		start(event);
+		document.addEventListener('mousemove',onMouseMove);
+		document.addEventListener('mouseup',onMouseUp);
+	}
+
+	function onTouchStart(event) {
+		event.preventDefault();
+		start(event);
+		let touches = event.touches;
+		if( touches.length !== 1 )
+			return;
+		let touch = touches[0];
+		touchX = touch.clientX;
+		touchY = touch.clientY;
+		document.addEventListener('touchmove',onTouchMove);
+		document.addEventListener('touchend',onMouseUp);
+	}
+
+	dad.setDraggable = function(el) {
+		el.setAttribute('dad-drag','');
+		el.setAttribute('draggable','false');
+		el.addEventListener('mousedown',onMouseDown);
+		el.addEventListener('touchstart',onTouchStart);
+	};
+
+	dad.setDropzone = function(el) {
+		el.setAttribute('dad-dropzone','');
+	};
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/delete_link.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,22 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Link = require "site:/lib/Link.luan"
+local User = require "site:/lib/User.luan"
+
+
+return function()
+	local user = User.current() or error()
+	local link_id = Http.request.parameters.link or error()
+	local link = Link.get_by_id(link_id)
+	if link ~= nil then
+		link.user_id == user.id or error()
+		link.delete()
+	end
+	Io.stdout = Http.response.text_writer()
+%>
+	let div = document.querySelector('div[link="<%=link_id%>"]');
+	if(div) div.outerHTML = '';
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/delete_pic.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,22 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Pic = require "site:/lib/Pic.luan"
+local User = require "site:/lib/User.luan"
+
+
+return function()
+	local user = User.current() or error()
+	local pic_id = Http.request.parameters.pic or error()
+	local pic = Pic.get_by_id(pic_id)
+	if pic ~= nil then
+		pic.user_id == user.id or error(user.id)
+		pic.delete()
+	end
+	Io.stdout = Http.response.text_writer()
+%>
+	//document.querySelector('span[pic="<%=pic_id%>"]').outerHTML = '';
+	location = '/pics.html';
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/delete_user.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,17 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local User = require "site:/lib/User.luan"
+
+
+return function()
+	local user = User.current()
+	if user ~= nil then
+		user.delete()
+	end
+	Io.stdout = Http.response.text_writer()
+%>
+	location = '/';
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/edit_icons.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,50 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local pairs = Luan.pairs or error()
+local String = require "luan:String.luan"
+local starts_with = String.starts_with or error()
+local substring = String.sub or error()
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Icon = require "site:/lib/Icon.luan"
+local icon_names = Icon.icon_names or error()
+local icon_from_doc = Icon.from_doc or error()
+local User = require "site:/lib/User.luan"
+local Db = require "site:/lib/Db.luan"
+
+
+return function()
+	local user = User.current() or error()
+	local user_id = user.id
+	local html = `%>
+				<form onsubmit="ajaxForm('/save_icons.js',this)" action="javascript:">
+<%
+	for name, info in pairs(icon_names) do
+		local doc = Db.get_document("+icon_user_id:"..user_id.." +icon_name:"..name)
+		local url = doc and icon_from_doc(doc).url or ""
+		local type = info.type or "url"
+		local label = type=="url" and "URL" or ""
+		if type=="email" and starts_with(url,"mailto:") then
+			url = substring(url,8)
+		end
+%>
+					<label><%=info.title%> <%=label%></label>
+					<div field>
+						<input type="<%=type%>" name="<%=name%>" value="<%=html_encode(url)%>" placeholder="<%= info.placeholder or "" %>">
+					</div>
+<%
+	end
+%>
+					<button small type=submit>Save</button>
+					<button small type=button onclick="ajax('/cancel_edit_icons.js')">Cancel</button>
+				</form>
+<%`
+	Io.stdout = Http.response.text_writer()
+%>
+	document.querySelector('div[icons]').innerHTML = <%= json_string(html) %>;
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/edit_link.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,37 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Link = require "site:/lib/Link.luan"
+
+
+return function()
+	local link_id = Http.request.parameters.link or error()
+	local link = Link.get_by_id(link_id)
+	Io.stdout = Http.response.text_writer()
+	if link == nil then
+%>
+		let div = document.querySelector('div[link="<%=link_id%>"]');
+		if(div) div.outerHTML = '';
+<%
+		return
+	end
+	local html = `%>
+				<form onsubmit="ajaxForm('/save_link.js',this)" action="javascript:">
+					<input type=hidden name=link value="<%=link_id%>">
+					<label>Title</label>
+					<input type=text required name=title value="<%=html_encode(link.title)%>">
+					<label>URL</label>
+					<input type=url required name=url value="<%=html_encode(link.url)%>">
+					<button small type=submit>Save</button>
+					<button small type=button onclick="cancel('<%=link.id%>')">Cancel</button>
+				</form>
+<%`
+%>
+	document.querySelector('div[link="<%=link_id%>"]').innerHTML = <%= json_string(html) %>;
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/error_log.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,60 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local String = require "luan:String.luan"
+local trim = String.trim or error()
+local regex = String.regex or error()
+local contains = String.contains or error()
+local Table = require "luan:Table.luan"
+local concat = Table.concat or error()
+local Http = require "luan:http/Http.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "error_log.js"
+
+
+local bad_agents = {
+	[[Googlebot]]
+	[[GSA/]]
+	[[Mobile/15E148 Instagram]]
+	[[Firefox/]]
+	[[VivoBrowser/]]
+	[[Chrome/(4\d|5\d|70|83|86|87|94)\.]]
+	[[musical_ly_2\d\.]]
+}
+local bad_agents_ptn = regex(concat(bad_agents,"|"))
+
+local bad_contents = {
+	[[\Qchrome-extension://\E]]
+	[[\Q@webkit-masked-url://hidden/\E]]
+	[[\Qanalytics.tiktok.com\E]]
+	[[\QStrict mode does not allow function declarations in a lexically nested statement.\E]]
+	[[\QFailed to set the 'currentTime' property on 'HTMLMediaElement': The provided double value is non-finite.\E]]
+	[[\QThe element has no supported sources.\E]]
+	[[\QThe play() request was interrupted by a call to pause().\E]]
+	[[\QReferenceError: Can't find variable: _AutofillCallbackHandler\E]]
+	[[\QUtilityScript\E]]
+	[[\QscrollReadRandom\E]]
+	[[\QdoGameClick\E]]
+	[[\Qurl = undefined\E]]
+	[[\QFailed to set remote answer sdp: The order of m-lines in answer doesn't match order in offer. Rejecting answer.\E]]
+}
+local bad_contents_ptn = regex(concat(bad_contents,"|"))
+
+local function priority(err)
+	local agent = Http.request.headers["user-agent"]
+	if agent~=nil and bad_agents_ptn.matches(agent) then return "info" end
+	if bad_contents_ptn.matches(err) then return "info" end
+	local x_requested_with = Http.request.headers["X-Requested-With"]
+	if x_requested_with ~= nil then
+		return "info"
+	end
+	return "error"
+end
+
+return function()
+	local err = Http.request.parameters.err
+	if err == nil then
+		return  -- stupid bots
+	end
+	local call = priority(err)
+	logger[call](trim(err).."\n"..trim(Http.request.raw_head).."\n")
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/facebook.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,82 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local stringify = Luan.stringify or error()
+local String = require "luan:String.luan"
+local trim = String.trim or error()
+local Table = require "luan:Table.luan"
+local copy = Table.copy or error()
+local Time = require "luan:Time.luan"
+local time_now = Time.now or error()
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local json_parse = Parsers.json_parse or error()
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local is_test = not Shared.is_production
+local has_facebook = not not Shared.has_facebook
+local Utils = require "site:/lib/Utils.luan"
+local to_list = Utils.to_list or error()
+local User = require "site:/lib/User.luan"
+local current_user = User.current or error()
+local Facebook = require "site:/lib/Facebook.luan"
+local call = Facebook.call or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "facebook.js"
+
+
+local function run()
+	if not has_facebook or is_test then return end
+	local user = current_user()
+	local request = Http.request
+	local user_data = {
+		fbp = request.cookies._fbp
+		fbc = request.cookies._fbc
+		client_user_agent = request.headers["User-Agent"]
+		client_ip_address = request.headers["X-Real-IP"]
+	}
+	if user ~= nil then
+		user_data.em = user.email or error()
+		user_data.external_id = user.id or error()
+	end
+	local request_parameters = request.parameters
+	local event = {
+		event_id = request_parameters.event_id or error()
+		event_time = time_now() // 1000
+		user_data = user_data
+		action_source = "website"
+		event_source_url = request.headers["Referer"]
+	}
+	local event_names = request_parameters.event_name or error()
+	event_names = to_list(event_names)
+	local events = {}
+	for _, event_name in ipairs(event_names) do
+		local ev = copy(event)
+		ev.event_name = event_name
+		local props = request_parameters[event_name]
+		if props ~= nil then
+			ev.custom_data = json_parse(props)
+		end
+		events[#events+1] = ev
+	end
+	-- logger.info(stringify(events))
+	try
+		local result = call(events)
+		logger.info(result)
+	catch e
+		local response_content = e.response_content
+		try
+			response_content = stringify(json_parse(response_content))
+		catch e2
+		end
+		logger.error(e.."\nresponse_content = "..e.response_content.."\nevents = "..stringify(events).."\n"..trim(request.raw_head).."\n")
+	end
+end
+
+return function()
+	try
+		run()
+	catch e
+		logger.error(e.."\n"..trim(Http.request.raw_head).."\n")
+	end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/forgot.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,34 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local page_header = Shared.page_header or error()
+local footer = Shared.footer or error()
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+	</head>
+	<body>
+		<form page onsubmit="ajaxForm('/forgot.js',this)" action="javascript:">
+<%			page_header() %>
+			<div>
+				<h1>Get log in info</h1>
+				<input type=email required name=email placeholder="Email">
+				<div error=email></div>
+				<button type=submit big>Send</button>
+			</div>
+<%			footer() %>
+		</form>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/forgot.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,33 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local User = require "site:/lib/User.luan"
+local Shared = require "site:/lib/Shared.luan"
+local js_error = Shared.js_error or error()
+local send_mail_async = Shared.send_mail_async or error()
+
+
+return function()
+	local email = Http.request.parameters.email or error()
+	local user = User.get_by_email(email)
+	Io.stdout = Http.response.text_writer()
+	if user == nil or user.registered == nil then
+		js_error( "email", "Email not found" )
+		return
+	end
+	send_mail_async {
+		From = "Link My Style <support@linkmy.style>"
+		To = email
+		Subject = "Account information"
+		body = `%>
+Here is your account information:
+username: <%=user.name%>
+password: <%=user.password%>
+<%`
+	}
+%>
+	clearErrors(context.form);
+	location = '/forgot2.html?email=<%=email%>';
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/forgot2.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,34 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local page_header = Shared.page_header or error()
+local footer = Shared.footer or error()
+
+
+return function()
+	local email = Http.request.parameters.email or error()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+	</head>
+	<body>
+		<div page>
+<%			page_header() %>
+			<div>
+				<h1>Email sent</h1>
+				<p>Your account information has been emailed to <%=email%>.</p>
+				<p>If you can't find the email, it might be in your Spam folder.</p>
+			</div>
+<%			footer() %>
+		</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/help.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,191 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local page_header = Shared.page_header or error()
+local footer = Shared.footer or error()
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+			[page] > * {
+				max-width: 700px;
+			}
+			h1 {
+				text-align: center;
+			}
+			div[row] {
+				border: solid #EEEEEE;
+				color: #4E4293;
+			}
+			div[row] + div[row] {
+				border-top: none;
+			}
+			div[row] > div {
+				padding: 10px;
+			}
+			div[q] {
+				display: flex;
+				justify-content: space-between;
+			}
+			div[q]:hover {
+				background-color: #DBD6FE;
+			}
+			div[a] {
+				display: none;
+			}
+			div[a] img {
+				width: 100%;
+			}
+			@media (min-width: 575px) {
+				div[a] > div {
+					display: flex;
+					align-items: flex-start;
+					gap: 10px;
+				}
+				div[a] img {
+					width: 40%;
+				}
+			}
+		</style>
+		<script>
+			function toggle(q) {
+				let a = q.nextElementSibling;
+				let span = q.querySelector('span:last-child');
+				if( a.style.display === 'block' ) {
+					a.style.display = 'none';
+					span.textContent = '⌄';
+					history.replaceState(null,null,'#');
+				} else {
+					a.style.display = 'block';
+					span.textContent = '^';
+					history.replaceState(null,null,`#${q.id}`);
+				}
+			}
+
+			function init() {
+				if( location.hash ) {
+					let q = document.querySelector(location.hash);
+					if(q)
+						toggle(q);
+				}
+			}
+		</script>
+	</head>
+	<body onload="init()">
+		<div page>
+<%			page_header() %>
+			<h1>Help</h1>
+			<div row>
+				<div q id=account onclick="toggle(this)">
+					<span>How to create an account</span>
+					<span>⌄</span>
+				</div>
+				<div a>
+					<div>
+						<div>
+							<p>When signing up for LinkMyStyle, all we ask is for an email, a username, and a password.</p>
+
+							<p>Once you’ve filled everything out and clicked on the “Create Account” button, we will send you a 6 digit verification code. Enter this code and you’re all set up.</p>
+						</div>
+						<img src="/images/help/register.jpg">
+					</div>
+				</div>
+			</div>
+			<div row>
+				<div q id=setup onclick="toggle(this)">
+					<span>How to set up your page</span>
+					<span>⌄</span>
+				</div>
+				<div a>
+					<div>
+						<div>
+							<p>First, click on the upper-right icon, and then click on “My account”. This will bring you to your account page. Here, you can add your profile picture, profile title, profile bio, change your username, change your password, add social icons, and change your email.</p>
+
+							<p>Once you’ve done this you can start adding links by clicking on the upper-right corner and clicking on “Links”. Title your link and input the URL. Once you’ve done this, you can grab the left of each link and rearrange as needed.</p>
+
+							<p>Next, to add images to your page, you can click on the upper-right corner and click “Photos”. Start by adding a title to your photo. This is just for your own analytics. Once you’ve named your photo, click “Add an image”, crop if needed, save the image, and then go ahead and add the associated links to the photo.</p>
+						</div>
+						<img src="/images/help/burger.jpg">
+					</div>
+				</div>
+			</div>
+			<div row>
+				<div q id=customize onclick="toggle(this)">
+					<span>How to customize your page</span>
+					<span>⌄</span>
+				</div>
+				<div a>
+					<div>
+						<div>
+							<p>Click on the upper-right icon and then click on “My theme” to edit the look of your page. Here you can upload any background image or change the background color. When changing the color of a section, you can either click on the left box showing the color or input a hex code of your choice. You can click the left side of every section to make quick changes there.</p>
+						</div>
+						<img src="/images/help/themepage.jpg">
+					</div>
+				</div>
+			</div>
+			<div row>
+				<div q id=affiliates onclick="toggle(this)">
+					<span>The best design for affiliates</span>
+					<span>⌄</span>
+				</div>
+				<div a>
+					<div>
+						<div>
+							<p><a href="https://linkmy.style/MidsizeFashionInspo ">MidsizeFashionInspo</a> is a great example of an affiliate who uses our site. Whenever she posts a video on TikTok or Instagram, she uploads a screenshot to her LinkMyStyle page. Once she does that, she links the products she shows in that video. This helps her followers easily find products and helps to increase her affiliate sales.</p>
+						</div>
+						<img src="/images/help/mid.jpg">
+					</div>
+				</div>
+			</div>
+			<div row>
+				<div q id=musicians onclick="toggle(this)">
+					<span>The best design for musicians</span>
+					<span>⌄</span>
+				</div>
+				<div a>
+					<div>
+						<div>
+							<p><a href="https://linkmy.style/JordanTaylor">JordanTaylor</a> is a great example of a musician who uses our site. If you click on her link, you can see that she displays her name, describes her work, links her social media, and then has her albums as photos. Each photo of an album cover also has links to each individual song. Since Jordan has a mix of songs on Spotify and Soundcloud, this makes it easy to mix and match platforms.</p>
+						</div>
+						<img src="/images/help/jordan.jpg">
+					</div>
+				</div>
+			</div>
+			<div row>
+				<div q id=file onclick="toggle(this)">
+					<span>How to link to a file</span>
+					<span>⌄</span>
+				</div>
+				<div a>
+					<div>
+						<div>
+							<p>To link to a file, you need a URL for the file.  If you have a cloud file storage service like <a href="https://support.google.com/docs/answer/183965?visit_id=638477670270957051-2553158566&rd=1">Google Drive</a> or <a href="https://help.dropbox.com/share/create-and-share-link">Dropbox</a>, you should be able to share the file and get a URL for it.  If you don't have such a service, you can look for a "file upload" service instead.</p>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div row>
+				<div q id=contact onclick="toggle(this)">
+					<span>Have another question?</span>
+					<span>⌄</span>
+				</div>
+				<div a>
+					<p>Contact us at <b>support@linkmy.style</b></p>
+				</div>
+			</div>
+<%			footer() %>
+		</div>
+	</body>
+</html>
+<%
+end
Binary file src/images/1.gif has changed
Binary file src/images/bag.jpg has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/close.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="m12.45 37.65-2.1-2.1L21.9 24 10.35 12.45l2.1-2.1L24 21.9l11.55-11.55 2.1 2.1L26.1 24l11.55 11.55-2.1 2.1L24 26.1Z"/></svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/drag_indicator.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M17.5 40q-1.45 0-2.475-1.025Q14 37.95 14 36.5q0-1.45 1.025-2.475Q16.05 33 17.5 33q1.45 0 2.475 1.025Q21 35.05 21 36.5q0 1.45-1.025 2.475Q18.95 40 17.5 40Zm13 0q-1.45 0-2.475-1.025Q27 37.95 27 36.5q0-1.45 1.025-2.475Q29.05 33 30.5 33q1.45 0 2.475 1.025Q34 35.05 34 36.5q0 1.45-1.025 2.475Q31.95 40 30.5 40Zm-13-12.5q-1.45 0-2.475-1.025Q14 25.45 14 24q0-1.45 1.025-2.475Q16.05 20.5 17.5 20.5q1.45 0 2.475 1.025Q21 22.55 21 24q0 1.45-1.025 2.475Q18.95 27.5 17.5 27.5Zm13 0q-1.45 0-2.475-1.025Q27 25.45 27 24q0-1.45 1.025-2.475Q29.05 20.5 30.5 20.5q1.45 0 2.475 1.025Q34 22.55 34 24q0 1.45-1.025 2.475Q31.95 27.5 30.5 27.5ZM17.5 15q-1.45 0-2.475-1.025Q14 12.95 14 11.5q0-1.45 1.025-2.475Q16.05 8 17.5 8q1.45 0 2.475 1.025Q21 10.05 21 11.5q0 1.45-1.025 2.475Q18.95 15 17.5 15Zm13 0q-1.45 0-2.475-1.025Q27 12.95 27 11.5q0-1.45 1.025-2.475Q29.05 8 30.5 8q1.45 0 2.475 1.025Q34 10.05 34 11.5q0 1.45-1.025 2.475Q31.95 15 30.5 15Z"/></svg>
\ No newline at end of file
Binary file src/images/favicon.png has changed
Binary file src/images/help/burger.jpg has changed
Binary file src/images/help/jordan.jpg has changed
Binary file src/images/help/mid.jpg has changed
Binary file src/images/help/register.jpg has changed
Binary file src/images/help/themepage.jpg has changed
Binary file src/images/home/01.png has changed
Binary file src/images/home/02.png has changed
Binary file src/images/home/03.png has changed
Binary file src/images/home/04.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/home/analytics.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 86 83" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-2245.05,-2624.3)">
+        <g transform="matrix(0.833333,0,0,3.84155,1884.49,76.8119)">
+            <g transform="matrix(0.67424,0,0,0.146261,417.265,659.361)">
+                <path d="M161.365,49.804L126.548,83.218C124.737,84.956 122.127,85.598 119.717,84.897L119.628,84.87L78.096,72.196L34.79,114.349C32.035,117.03 27.644,116.998 24.929,114.297L24.846,114.215C22.165,111.459 22.198,107.069 24.899,104.353L24.981,104.271L71.268,59.219C73.079,57.456 75.708,56.8 78.135,57.506L78.224,57.533L119.787,70.216L151.363,39.913L141.173,39.913C136.486,39.913 134.142,37.569 134.142,32.882C134.142,28.194 136.486,25.85 141.173,25.85L167.931,25.85C170.429,25.85 172.262,26.516 173.429,27.848C174.761,29.015 175.427,30.848 175.427,33.347L175.427,60.105C175.427,64.792 173.084,67.136 168.396,67.136C163.709,67.136 161.365,64.792 161.365,60.105L161.365,49.804ZM80.273,96.68C84.961,96.68 87.305,99.023 87.305,103.711L87.305,165.625C87.305,170.313 84.961,172.656 80.273,172.656C75.586,172.656 73.242,170.313 73.242,165.625L73.242,103.711C73.242,99.023 75.586,96.68 80.273,96.68ZM119.727,106.445C124.414,106.445 126.758,108.789 126.758,113.477L126.758,165.625C126.758,170.313 124.414,172.656 119.727,172.656C115.039,172.656 112.695,170.313 112.695,165.625L112.695,113.477C112.695,108.789 115.039,106.445 119.727,106.445ZM159.18,89.453C163.867,89.453 166.211,91.797 166.211,96.484L166.211,165.625C166.211,170.313 163.867,172.656 159.18,172.656C154.492,172.656 152.148,170.313 152.148,165.625L152.148,96.484C152.148,91.797 154.492,89.453 159.18,89.453ZM40.82,131.445C45.508,131.445 47.852,133.789 47.852,138.477L47.852,165.625C47.852,170.313 45.508,172.656 40.82,172.656C36.133,172.656 33.789,170.313 33.789,165.625L33.789,138.477C33.789,133.789 36.133,131.445 40.82,131.445Z" style="fill:white;"/>
+            </g>
+        </g>
+    </g>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/home/background.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 98 91" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-1702.15,-2624.3)">
+        <g transform="matrix(0.833333,0,0,3.84155,1341.59,76.8119)">
+            <g transform="matrix(0.689989,0,0,0.149677,422.22,660.022)">
+                <path d="M71.897,25.497C122.406,8.418 173.4,40.721 182.848,84.2C185.788,97.715 185.394,108.576 182.267,117.109C179.504,124.889 173.823,131.299 166.43,134.976C153.297,141.636 136.812,139.764 127.503,136.4C121.333,134.176 116.527,133.879 113.721,134.703C112.461,135.079 111.8,135.612 111.418,136.133C111.03,136.679 110.545,137.715 110.545,139.745C110.545,141.836 111.03,143.97 111.818,146.4C112.23,147.661 112.667,148.914 113.127,150.158C113.588,151.436 114.085,152.848 114.491,154.285C115.303,157.133 115.945,160.636 115.097,164.394C114.212,168.303 111.903,171.667 108.309,174.588C105.721,176.685 102.448,177.679 99.333,178.109C96.133,178.558 92.545,178.503 88.806,178.073C81.327,177.206 72.6,174.758 63.927,170.909C46.764,163.297 28.406,149.564 20.818,129.861C4.479,87.455 24.067,41.667 71.897,25.497ZM171.006,86.77C163.109,50.442 119.515,22.2 75.776,36.988C34.448,50.958 18.279,89.564 32.127,125.509C38.097,141.006 53.176,152.891 68.842,159.836C76.588,163.273 84.127,165.339 90.2,166.036C93.236,166.388 95.752,166.382 97.655,166.115C99.636,165.836 100.467,165.345 100.667,165.182C102.642,163.582 103.115,162.43 103.273,161.727C103.473,160.861 103.418,159.667 102.836,157.606C102.443,156.266 101.999,154.942 101.503,153.636C101.115,152.564 100.685,151.364 100.291,150.145C99.333,147.206 98.424,143.685 98.424,139.752C98.424,135.745 99.406,132.042 101.624,129C103.855,125.945 106.958,124.067 110.291,123.085C116.685,121.2 124.412,122.406 131.612,125.006C139.018,127.673 151.794,128.812 160.945,124.17C165.605,121.877 169.178,117.838 170.885,112.933C172.976,107.236 173.636,98.879 171.006,86.782L171.006,86.77ZM81.818,60.243C81.818,65.23 77.714,69.335 72.727,69.335C67.74,69.335 63.636,65.23 63.636,60.243C63.637,55.256 67.741,51.153 72.727,51.153C77.714,51.153 81.817,55.256 81.818,60.243ZM63.636,102.667C63.636,107.654 59.533,111.759 54.545,111.759C49.558,111.759 45.455,107.654 45.455,102.667C45.455,97.681 49.559,93.577 54.545,93.577C59.532,93.577 63.636,97.681 63.636,102.667ZM136.364,66.304C136.364,71.291 132.26,75.395 127.273,75.395C122.286,75.395 118.182,71.291 118.182,66.304C118.183,61.317 122.286,57.213 127.273,57.213C132.259,57.213 136.363,61.317 136.364,66.304Z" style="fill:white;"/>
+            </g>
+        </g>
+    </g>
+</svg>
Binary file src/images/home/bio.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/home/desktop.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 1600 5000" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(0.833333,0,0,4.62963,0,0)">
+        <rect id="desktop" x="0" y="0" width="1920" height="1080" style="fill:none;"/>
+        <clipPath id="_clip1">
+            <rect id="desktop1" serif:id="desktop" x="0" y="0" width="1920" height="1080"/>
+        </clipPath>
+        <g clip-path="url(#_clip1)">
+            <g id="base" transform="matrix(2.83098,0,0,1.10501,-4514.35,-209.818)">
+                <rect x="1594.62" y="189.878" width="678.21" height="977.364" style="fill:white;"/>
+            </g>
+            <g id="deco3" transform="matrix(2.29286,-0.14202,0.788999,0.412715,-1327.29,529.092)">
+                <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:rgb(247,249,251);"/>
+                <clipPath id="_clip2">
+                    <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z"/>
+                </clipPath>
+                <g clip-path="url(#_clip2)">
+                    <g transform="matrix(0.401883,0.138292,-0.138292,0.401883,615.164,-517.482)">
+                        <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:url(#_Linear3);"/>
+                    </g>
+                </g>
+            </g>
+            <g id="deco2" transform="matrix(0.659866,0,-2.31135e-15,0.221239,743.955,476.71)">
+                <clipPath id="_clip4">
+                    <path d="M871.351,214.608C926.207,197.605 993.793,197.605 1048.65,214.608C1129.67,239.722 1248.87,276.668 1329.89,301.781C1384.75,318.784 1418.54,350.208 1418.54,384.214L1418.54,558.56C1418.54,592.567 1384.75,623.99 1329.89,640.993C1248.87,666.107 1129.67,703.052 1048.65,728.166C993.793,745.169 926.207,745.169 871.351,728.166C790.329,703.052 671.134,666.107 590.111,640.993C535.255,623.99 501.462,592.567 501.462,558.56L501.462,384.214C501.462,350.208 535.255,318.784 590.111,301.781C671.134,276.668 790.329,239.722 871.351,214.608Z"/>
+                </clipPath>
+                <g clip-path="url(#_clip4)">
+                    <g opacity="0.2">
+                        <g transform="matrix(1.48164,0,0,0.795442,-310.97,-138.468)">
+                            <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:url(#_Linear5);"/>
+                        </g>
+                        <g transform="matrix(1.23004,3.5038e-17,-9.10989e-16,0.660364,-256.794,-80.6764)">
+                            <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:white;fill-opacity:0.1;"/>
+                        </g>
+                        <g transform="matrix(1.23004,3.5038e-17,-9.10989e-16,0.660364,251.144,126.597)">
+                            <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:white;fill-opacity:0.1;"/>
+                        </g>
+                    </g>
+                </g>
+            </g>
+            <g id="deco1" transform="matrix(4.12765,-0.255667,1.42037,0.742978,-3465.22,-293.854)">
+                <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:rgb(238,243,247);"/>
+            </g>
+            <g id="deco0" transform="matrix(2.14826,-0.133063,1.37695,0.720266,-1712.93,-203.84)">
+                <clipPath id="_clip6">
+                    <path d="M871.351,214.608C926.207,197.605 993.793,197.605 1048.65,214.608C1129.67,239.722 1248.87,276.668 1329.89,301.781C1384.75,318.784 1418.54,350.208 1418.54,384.214L1418.54,558.56C1418.54,592.567 1384.75,623.99 1329.89,640.993C1248.87,666.107 1129.67,703.052 1048.65,728.166C993.793,745.169 926.207,745.169 871.351,728.166C790.329,703.052 671.134,666.107 590.111,640.993C535.255,623.99 501.462,592.567 501.462,558.56L501.462,384.214C501.462,350.208 535.255,318.784 590.111,301.781C671.134,276.668 790.329,239.722 871.351,214.608Z"/>
+                </clipPath>
+                <g clip-path="url(#_clip6)">
+                    <g transform="matrix(1.48164,0,0,0.795442,-310.97,-138.468)">
+                        <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:rgb(181,169,255);"/>
+                    </g>
+                    <g transform="matrix(1.23004,3.5038e-17,-9.10989e-16,0.660364,-256.794,-80.6764)">
+                        <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:white;fill-opacity:0.05;"/>
+                    </g>
+                    <g transform="matrix(1.23004,3.5038e-17,-9.10989e-16,0.660364,251.144,126.597)">
+                        <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:white;fill-opacity:0.05;"/>
+                    </g>
+                </g>
+            </g>
+            <g transform="matrix(0.606852,0,0,0.109233,1225.03,992.113)">
+                <path d="M184.375,28.613L15.625,28.613C6.996,28.613 0,35.609 0,44.238L0,159.473C0,168.102 6.996,175.098 15.625,175.098L184.375,175.098C193.004,175.098 200,168.102 200,159.473L200,44.238C200,35.609 193.004,28.613 184.375,28.613ZM15.625,38.379L184.375,38.379C187.606,38.379 190.234,41.007 190.234,44.238L190.234,56.075L100,105.337L9.766,56.075L9.766,44.238C9.766,41.007 12.394,38.379 15.625,38.379ZM184.375,165.332L15.625,165.332C12.394,165.332 9.766,162.704 9.766,159.473L9.766,67.201L97.648,115.179C98.369,115.574 99.179,115.779 100.001,115.776C100.797,115.779 101.604,115.587 102.352,115.179L190.234,67.201L190.234,159.473C190.234,162.704 187.606,165.332 184.375,165.332Z" style="fill:white;fill-rule:nonzero;"/>
+            </g>
+            <g transform="matrix(0.117846,0,0,0.0212123,846.986,996.572)">
+                <g transform="matrix(201.566,0,0,201.566,4161.21,1014.83)">
+                </g>
+                <text x="3198.16px" y="1014.83px" style="font-family:'Helvetica-Bold', 'Helvetica';font-weight:700;font-size:201.566px;fill:white;">EMAIL US</text>
+            </g>
+        </g>
+    </g>
+    <defs>
+        <linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(312.385,538.276,-538.276,312.385,693.479,500.292)"><stop offset="0" style="stop-color:rgb(145,129,238);stop-opacity:0.6"/><stop offset="1" style="stop-color:rgb(111,141,251);stop-opacity:0.6"/></linearGradient>
+        <linearGradient id="_Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(312.385,538.276,-538.276,312.385,693.479,500.292)"><stop offset="0" style="stop-color:rgb(145,129,238);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(111,141,251);stop-opacity:1"/></linearGradient>
+    </defs>
+</svg>
Binary file src/images/home/hyn_x.jpeg has changed
Binary file src/images/home/i1.png has changed
Binary file src/images/home/i2.png has changed
Binary file src/images/home/i3.png has changed
Binary file src/images/home/i4.png has changed
Binary file src/images/home/i5.png has changed
Binary file src/images/home/i6.png has changed
Binary file src/images/home/ig_midsize1.png has changed
Binary file src/images/home/ins.png has changed
Binary file src/images/home/kenzie.jpeg has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/home/mail.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 200 219" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-2299.15,-3535.51)">
+        <g transform="matrix(0.833333,0,0,3.65741,1341.59,1.81899e-12)">
+            <g id="email_us" transform="matrix(1.18688,0,0,0.793257,-237.164,-159.077)">
+                <g transform="matrix(0.326694,0,-6.14352e-16,0.111373,988.822,1371.5)">
+                    <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:url(#_Linear1);"/>
+                </g>
+                <g transform="matrix(0.0992904,0,0,0.033849,899.415,1441.64)">
+                    <g transform="matrix(201.566,0,0,201.566,4161.21,1014.83)">
+                    </g>
+                    <text x="3198.16px" y="1014.83px" style="font-family:'Helvetica-Bold', 'Helvetica';font-weight:700;font-size:201.566px;fill:white;">EMAIL US</text>
+                </g>
+                <g transform="matrix(0.5113,0,0,0.174307,1217.93,1434.52)">
+                    <path d="M184.375,28.613L15.625,28.613C6.996,28.613 0,35.609 0,44.238L0,159.473C0,168.102 6.996,175.098 15.625,175.098L184.375,175.098C193.004,175.098 200,168.102 200,159.473L200,44.238C200,35.609 193.004,28.613 184.375,28.613ZM15.625,38.379L184.375,38.379C187.606,38.379 190.234,41.007 190.234,44.238L190.234,56.075L100,105.337L9.766,56.075L9.766,44.238C9.766,41.007 12.394,38.379 15.625,38.379ZM184.375,165.332L15.625,165.332C12.394,165.332 9.766,162.704 9.766,159.473L9.766,67.201L97.648,115.179C98.369,115.574 99.179,115.779 100.001,115.776C100.797,115.779 101.604,115.587 102.352,115.179L190.234,67.201L190.234,159.473C190.234,162.704 187.606,165.332 184.375,165.332Z" style="fill:white;fill-rule:nonzero;"/>
+                </g>
+            </g>
+        </g>
+    </g>
+    <defs>
+        <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(312.385,538.276,-538.276,312.385,693.479,500.292)"><stop offset="0" style="stop-color:rgb(145,129,238);stop-opacity:0.6"/><stop offset="1" style="stop-color:rgb(111,141,251);stop-opacity:0.6"/></linearGradient>
+    </defs>
+</svg>
Binary file src/images/home/midsize.jpeg has changed
Binary file src/images/home/midsize1.png has changed
Binary file src/images/home/midsize2.png has changed
Binary file src/images/home/midsize3.png has changed
Binary file src/images/home/midsize4.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/home/mobile.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 1077 7600" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1.0937,0,0,3.19439,-4165.52,-11161.3)">
+        <rect id="mobile" x="3808.64" y="3494.02" width="984.285" height="2379.17" style="fill:none;"/>
+        <clipPath id="_clip1">
+            <rect id="mobile1" serif:id="mobile" x="3808.64" y="3494.02" width="984.285" height="2379.17"/>
+        </clipPath>
+        <g clip-path="url(#_clip1)">
+            <g transform="matrix(1.30624,0,0,1.86078,-1467.11,-3411.58)">
+                <rect x="4037.67" y="3731.33" width="755.965" height="1140.64" style="fill:white;"/>
+            </g>
+            <g id="deco3" transform="matrix(-1.74702,-0.205829,-0.601167,0.598148,6198.8,4299.09)">
+                <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:rgb(247,249,251);"/>
+                <clipPath id="_clip2">
+                    <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z"/>
+                </clipPath>
+                <g clip-path="url(#_clip2)">
+                    <g transform="matrix(0.401883,0.138292,-0.138292,0.401883,615.164,-517.482)">
+                        <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:url(#_Linear3);"/>
+                    </g>
+                </g>
+            </g>
+            <g id="deco2" transform="matrix(0.502776,0,-1.7611e-15,0.320642,3857.92,4223.17)">
+                <clipPath id="_clip4">
+                    <path d="M871.351,214.608C926.207,197.605 993.793,197.605 1048.65,214.608C1129.67,239.722 1248.87,276.668 1329.89,301.781C1384.75,318.784 1418.54,350.208 1418.54,384.214L1418.54,558.56C1418.54,592.567 1384.75,623.99 1329.89,640.993C1248.87,666.107 1129.67,703.052 1048.65,728.166C993.793,745.169 926.207,745.169 871.351,728.166C790.329,703.052 671.134,666.107 590.111,640.993C535.255,623.99 501.462,592.567 501.462,558.56L501.462,384.214C501.462,350.208 535.255,318.784 590.111,301.781C671.134,276.668 790.329,239.722 871.351,214.608Z"/>
+                </clipPath>
+                <g clip-path="url(#_clip4)">
+                    <g opacity="0.2">
+                        <g transform="matrix(1.48164,0,0,0.795442,-310.97,-138.468)">
+                            <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:url(#_Linear5);"/>
+                        </g>
+                        <g transform="matrix(1.23004,3.5038e-17,-9.10989e-16,0.660364,-256.794,-80.6764)">
+                            <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:white;fill-opacity:0.1;"/>
+                        </g>
+                        <g transform="matrix(1.23004,3.5038e-17,-9.10989e-16,0.660364,251.144,126.597)">
+                            <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:white;fill-opacity:0.1;"/>
+                        </g>
+                    </g>
+                </g>
+            </g>
+            <g transform="matrix(1,0,0,0.907895,0,540.381)">
+                <g transform="matrix(0.914324,0,0,0.344807,-8413.16,1425.95)">
+                    <use xlink:href="#_Image6" x="13371" y="9901" width="1073px" height="1073px" opacity="0.5"/>
+                </g>
+                <g transform="matrix(0.490096,-0.0495234,0.244606,0.344264,3714.98,4786.13)">
+                    <clipPath id="_clip7">
+                        <path d="M871.351,214.608C926.207,197.605 993.793,197.605 1048.65,214.608C1129.67,239.722 1248.87,276.668 1329.89,301.781C1384.75,318.784 1418.54,350.208 1418.54,384.214L1418.54,558.56C1418.54,592.567 1384.75,623.99 1329.89,640.993C1248.87,666.107 1129.67,703.052 1048.65,728.166C993.793,745.169 926.207,745.169 871.351,728.166C790.329,703.052 671.134,666.107 590.111,640.993C535.255,623.99 501.462,592.567 501.462,558.56L501.462,384.214C501.462,350.208 535.255,318.784 590.111,301.781C671.134,276.668 790.329,239.722 871.351,214.608Z"/>
+                    </clipPath>
+                    <g clip-path="url(#_clip7)">
+                        <g opacity="0.3">
+                            <g transform="matrix(1.48164,0,0,0.795442,-310.97,-138.468)">
+                                <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:url(#_Linear8);"/>
+                            </g>
+                            <g transform="matrix(1.23004,3.5038e-17,-9.10989e-16,0.660364,-256.794,-80.6764)">
+                                <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:white;fill-opacity:0.1;"/>
+                            </g>
+                            <g transform="matrix(1.23004,3.5038e-17,-9.10989e-16,0.660364,251.144,126.597)">
+                                <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:white;fill-opacity:0.1;"/>
+                            </g>
+                        </g>
+                    </g>
+                </g>
+            </g>
+            <g transform="matrix(0.827425,0,0,0.282153,568.723,2615.44)">
+                <g transform="matrix(3.13487,-1.08311,1.07874,3.14756,1176.87,2272.97)">
+                    <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:rgb(238,243,247);"/>
+                </g>
+                <g transform="matrix(1.54783,-0.534783,0.992102,2.89476,2519.46,2613.77)">
+                    <clipPath id="_clip9">
+                        <path d="M871.351,214.608C926.207,197.605 993.793,197.605 1048.65,214.608C1129.67,239.722 1248.87,276.668 1329.89,301.781C1384.75,318.784 1418.54,350.208 1418.54,384.214L1418.54,558.56C1418.54,592.567 1384.75,623.99 1329.89,640.993C1248.87,666.107 1129.67,703.052 1048.65,728.166C993.793,745.169 926.207,745.169 871.351,728.166C790.329,703.052 671.134,666.107 590.111,640.993C535.255,623.99 501.462,592.567 501.462,558.56L501.462,384.214C501.462,350.208 535.255,318.784 590.111,301.781C671.134,276.668 790.329,239.722 871.351,214.608Z"/>
+                    </clipPath>
+                    <g clip-path="url(#_clip9)">
+                        <g transform="matrix(1.48164,0,0,0.795442,-310.97,-138.468)">
+                            <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:rgb(181,169,255);"/>
+                        </g>
+                        <g transform="matrix(1.23004,3.5038e-17,-9.10989e-16,0.660364,-256.794,-80.6764)">
+                            <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:white;fill-opacity:0.1;"/>
+                        </g>
+                        <g transform="matrix(1.23004,3.5038e-17,-9.10989e-16,0.660364,251.144,126.597)">
+                            <path d="M797.982,443.875C835.006,422.499 880.621,422.499 917.645,443.875C972.33,475.447 1052.78,521.894 1107.46,553.465C1144.49,574.841 1167.29,614.345 1167.29,657.097L1167.29,876.278C1167.29,919.03 1144.49,958.534 1107.46,979.91C1052.78,1011.48 972.33,1057.93 917.645,1089.5C880.621,1110.88 835.006,1110.88 797.982,1089.5C743.298,1057.93 662.85,1011.48 608.165,979.91C571.141,958.534 548.334,919.03 548.334,876.278L548.334,657.097C548.334,614.345 571.141,574.841 608.165,553.465C662.85,521.894 743.298,475.447 797.982,443.875Z" style="fill:white;fill-opacity:0.1;"/>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+    <defs>
+        <linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(312.385,538.276,-538.276,312.385,693.479,500.292)"><stop offset="0" style="stop-color:rgb(145,129,238);stop-opacity:0.6"/><stop offset="1" style="stop-color:rgb(111,141,251);stop-opacity:0.6"/></linearGradient>
+        <linearGradient id="_Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(312.385,538.276,-538.276,312.385,693.479,500.292)"><stop offset="0" style="stop-color:rgb(145,129,238);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(111,141,251);stop-opacity:1"/></linearGradient>
+        <image id="_Image6" width="1073px" height="1073px" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDEAAAQxCAYAAAA3ChaNAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nOzdedBvd2Hf98+92jjAAetosSQQGMxilrCDWQ1oiYkXmsTTuMVOAq2XOk48qZfatdO6SW2HjB2PO1OSJukkdty6HWfsJjbtJJauEELsYGGL3SAWoRV0JHEkHW333v7xPJff81zu8izn9/uec36v18wdtFw9v4+HGY94z/f7/SUAAAAAE3Cg9AAA2Im26w8muSDJxUkuSVInqXb46zFJzkrycJIHt/x66DR/fk+S2zZ/3d7U1YPL/78UAICTETEAKKrt+gPZiBOXbP66+Lj/PPbHFyU5s9DMY7ZFjS1/fOzPb01yU1NXDxdbCAAwYyIGACuxGSsuSfLcJM/b/PXczV/fUnDa0A4n+Ysknzzu12ebuupLDgMAmDoRA4BBbYkVW0PFsf98YsFppR1NclO2h41PJflkU1f3lxwGADAVIgYA+9J2/VlJXpzktZu/XpPkwqKjpuVwkhuSXL/5671NXd1edhIAwDiJGADsStv1T0jyqiyCxSuz8Xgmw/l8kvdmETY+09TVkbKTAADKEzEAOKW26y9K8vosTlq8IMnBoqPWT5uNqHEsbHzY46EAwDoSMQDYZvNNixckeXOS70/y8rKLOIH7k7w7yVWbvz7Z1NXRspMAAJZPxAAgbdefk43TFsfCxVPKLmKXbkty9bFfTV3dWngPAMBSiBgAa6rt+vOSfE82wsWbkjy+7CIG9IlsBI2rklzX1FVXeA8AwCBEDIA10nb9hUnekuSvZ+NRTm9bzN+jSd6fjaBxdTbe03i07CQAgL0RMQBmru36s7Nx4uKtSb43yZlFB1Ha15O8K4uo8VnvaQAAUyFiAMxU2/Uvyka4+KEk55ddw4jdnEXQONTU1Z2F9wAAnJSIATAjW66LvDXJC8uuYaL+LItvPbm+qasHCu8BAPgGEQNg4tquPysb10TeGtdFGNbDSa7P4pHQG5q6Olx2EgCwzkQMgIlqu/4JSX4kyd9PcmnhOayHNsk12bx+0tTVTYX3AABrRsQAmJi26y9N8lNJfizJEwrPYb3dlMV7Gtc0ddUW3gMAzJyIATARbde/OMnPJPnBuDLC+BxN8tEs3tN4X1NXD5WdBADMjYgBMGJt1x9M8qZsxIvLCs+B3eiTXJfFexo3NnV1pOwkAGDqRAyAEWq7/jHZ+GrUn07y3MJzYAh3JjmUxXsaNxfeAwBMkIgBMCJt15+Z5G1J/qckl5RdA0v1mSxOaVzb1NW9hfcAABMgYgCMwOa1kR9I8itJnlV4Dqza4SQfyuKR0A80dfVI2UkAwBiJGACFtV1/ZZJ/nOSlpbfASNyX5NosTmp8qqmro0UXAQCjIGIAFNJ2/SuyES882AmndmsWQeNQU1e3Fd4DABQiYgCsWNv135HkV5P89dJbYKI+nkXUuK6pq/sK7wEAVkTEAFiRtusvTfLL2Xi482DhOTAXjyR5fxbvaXykqatHy04CAJZFxABYsrbrz0ry32YjYDy28ByYu3uTvCsbUeOqJJ/zngYAzIeIAbBEbde/Jsn/luT5pbfAmvpyFqc0DjV19dXCewCAfRAxAJag7frzkrw9yY+U3gJsc0MW72lc39RVX3gPALALIgbAgNquP5Dkbyb5p0nOLzwHOLWHklyfxUmNG5q6OlJ2EgBwKiIGwEA2v3Xknyd5Q+EpwN7cleSabL6n0dTVF8vOAQCOJ2IA7FPb9VWSX0zy80nOKjwHGM7nszilcU1TV3cX3gMAa0/EANiHtusvS/Ivk3x76S3AUh1J8tEsvvXk/U1dPVR2EgCsHxEDYA/arj87ya8k+dn4/6Wwjh5Icl0WJzVu9FWuALB8/sUbYJc23774vSQvLr0FGI07khzKZtRo6uorhfcAwCyJGAA7tPnNIz+a5LeSVIXnAOP26SxOaVzb1NXXC+8BgFkQMQB2oO3685P8qyR/tfQWYHIOJ/lgFu9pfKipq0fKTgKAaRIxAE6j7forkvzbJBeX3gLMQpfk2myc0rgqyae9pwEAOyNiAJxE2/XnJPnVJD9Tegswa7dkETQONXV1e+E9ADBaIgbACXi8EyjoxiyixnVNXd1feA8AjIaIAXCctuv/WpLfTfK40luAtfdIkvdl8Z7GR5u6Olx2EgCUI2IAbGq7/mCSf5DkH5beAnAS9yS5JouTGp/3ngYA60TEAEjSdv3jkvxOkh8ovQVgF76UxSmNa5q6+lrhPQCwVCIGsPbarn9qkv+Q5IWltwDsw9EkN2RxSuP6pq4eLDsJAIYlYgBrre3670ryB0nOL70FYGAPJrk+G0Hj6iQfa+rqSNlJALA/Igawttqu/7Ek70hyZuktACvwtSSHsnlSo6mrLxXeAwC7JmIAa6ft+rOS/FaSv1N6C0BBn8viPY13NXV1T+E9AHBaIgawVtquPz/Jv0vyhsJTAMbkSJIPZ/Gexvubunq47CQA+GYiBrA2Nh/wvDrJM0pvARi5B5K8O4v3ND7uq1wBGAMRA1gLbdc/Jxv/Mv6k0lsAJuiOLE5pXN3U1S2F9wCwpkQMYPbarn9pkv8Y30ACMJRPZXFK49qmrrrCewBYEyIGMGubX6H6ziR16S0AM/Vokg9kcVLjw01dPVJ2EgBzJWIAs9V2/fck+YMkjym9BWCNdEnelcVJjc94TwOAoYgYwCy1Xf9fJPndJGeW3gKw5r6SxSmNQ01d3VF4DwATJmIAs9N2/Y8n+efx/+MAxujPszilcV1TVw8U3gPAhPgXfGBW2q7/+SRvL70DgB15OMn7shE1rkryp01dHS47CYAxEzGAWWi7/kCSX0vyC6W3ALBndye5Jouvcv184T0AjIyIAUzeZsD4jSQ/XXoLAIP6QhbvaVzT1NVdhfcAUJiIAUyagAGwNo4m+dMs3tN4b1NXD5adBMCqiRjAZAkYAGvtwSTvyeI9jT9v6upI2UkALJuIAUySgAHAcb6a5FA2r580dfXlwnsAWAIRA5gcAQOAHfhsFu9pXNvU1T2F9wAwABEDmBQBA4A9OJLkQ1lEjQ80dfVw2UkA7IWIAUyGgAHAQO5P8u4sHgn9RFNXR8tOAmAnRAxgEgQMAJbotmzEjKuTXN3U1a2F9wBwEiIGMHoCBgAr9sksvvXkuqauusJ7ANgkYgCjJmAAUNijSd6fxXsaH27q6tGykwDWl4gBjJaAAcAIfT3Ju7J4T+Oz3tMAWB0RAxglAQOAibg5i6BxqKmrOwvvAZg1EQMYHQEDgAn7syze07i+qasHCu8BmBURAxgVAQOAGXk4yfVZvKdxQ1NXh8tOApg2EQMYDQEDgJlrk1yTzesnTV3dVHgPwOSIGMAoCBgArKGbsnhP45qmrtrCewBGT8QAihMwACBHk3w0i/c03tfU1UNlJwGMj4gBFCVgAMAJ9Umuy+I9jRubujpSdhJAeSIGUIyAAQA7dmeSQ1m8p3Fz4T0ARYgYQBECBgDsy2eyeE/j2qau7i28B2AlRAxg5QQMABjU4SQfyuI9jQ82dfVI2UkAyyFiACslYADA0t2X5Nos3tP4VFNXR4suAhiIiAGsjIABAEXcmkXQONTU1W2F9wDsmYgBrISAAQCj8fEs3tN4d1NX9xfeA7BjIgawdAIGAIzWI0nen8V7Gh9t6urRspMATk7EAJZqM2D8epKfKb0FADite5Nck8X1k895TwMYExEDWBoBAwAm70tZBI1rmrr6auE9wJoTMYClEDAAYJZuyCJqXN/UVV94D7BmRAxgcAIGAKyFh5Jcn8UjoTc0dXWk7CRg7kQMYFACBgCsrbuSHMrmSY2mrr5Ydg4wRyIGMBgBAwDY4vNZfOvJu5q6urvwHmAGRAxgEAIGAHAKR5J8JIv3NN7f1NVDZScBUyRiAPsmYAAAu/RAkuuyeE/jRl/lCuyEiAHsi4ABAAzgjmy8p3FVkqubuvpK4T3ASIkYwJ4JGADAknw6i1Ma1zZ19fXCe4CREDGAPREwAIAVOZzkA1m8p/Ghpq4eKTsJKEXEAHZNwAAACuqSXJvFSY1Pe08D1oeIAeyKgAEAjMwtWZzSONTU1e2F9wBLJGIAOyZgAAATcGMWpzSua+rq/sJ7gAGJGMCOCBgAwAQ9kuR92YgaVyX5aFNXh8tOAvZDxABOS8AAAGbiniTXZHH95PPe04BpETGAUxIwAIAZ+1I2YsY7k1zt6gmMn4gBnJSAAQCskQeTHEryR0ne2dTVrYX3ACcgYgAnJGAAAGvuw9kIGn+U5EbXTmAcRAzgmwgYAADbfDmLoPHupq4eLrwH1paIAWwjYAAAnFKX5N8n+e0k1zZ1daTsHFgvIgbwDQIGAMCufDHJ7yT5naauvlB4C6wFEQNIImAAAOzTtUn+TZI/8C0nsDwiBiBgAAAMp0vy+9m4bvJeD4LCsEQMWHMCBgDA0vxFNmLGv27q6vbCW2AWRAxYYwIGAMBKPJzkd5P806auPlV6DEyZiAFrSsAAACjindn4d7D3uGoCuydiwBoSMAAAivtwNv597A+bujpcegxMhYgBa0bAAAAYlS8k+c0k/8a3msDpiRiwRgQMAIDRapP8syT/a1NXd5QeA2MlYsCaEDAAACbhwSS/leSfNHV1T+kxMDYiBqwBAQMAYHLuTvJr2TiZ8WDpMTAWIgbMnIABADBpNyf55ST/1gOgIGLArAkYAACz8Ykkv5jkj301K+tMxICZEjAAAGbpvUl+vqmr95YeAiWIGDBDAgYAwOz9UZJfbOrqE6WHwCqJGDAzAgYAwNo4ko1vMvnlpq7uKz0GVkHEgBkRMAAA1tLNSX6yqas/Lj0Elk3EgJkQMAAA1t4fJvmppq5uKT0ElkXEgBkQMAAA2NQl+aUk/8xXsjJHIgZMnIABAMAJfCTJjzV1dUPpITAkEQMmTMAAAOAUjiT5X5L8jx7+ZC5EDJgoAQMAgB26OclPNHX1/5YeAvslYsAECRgAAOzBO5L8XFNXfekhsFciBkyMgAEAwD58Isl/2dTVjaWHwF6IGDAhAgYAAAN4KMnPJnlHU1dHS4+B3RAxYCIEDAAABvbOJP9VU1dfLT0EdkrEgAkQMAAAWJLbk/ytpq6uKj0EdkLEgJETMAAAWIHfSPJLTV09XHoInIqIASMmYAAAsEJ/muQtTV19pvQQOJmDpQcAJyZgAACwYi9J8pG2699cegicjJMYMEICBgAABR1N8ktJ3u7bSxgbEQNGRsAAAGAk/q8k/3VTV33pIXCMiAEjImAAADAyH03ynzV1dUvpIZB4EwNGQ8AAAGCEXpqNdzK+s/QQSEQMGAUBAwCAEbsoybvbrv+bpYeA6yRQmIABAMCE/HqS/76pq8Olh7CeRAwoSMAAAGCC/r8kb2nq6t7SQ1g/IgYUImAAADBhH0vy3U1d3Vl6COtFxIACBAwAAGbgs0mubOrqy6WHsD5EDFgxAQMAgBm5ORsh4zOlh7AefDsJrJCAAQDAzFya5D1t17+o9BDWg4gBKyJgAAAwUxckubbt+teUHsL8iRiwAgIGAAAz98Qkf9J2/V8uPYR5EzFgyQQMAADWxGOTvLPt+h8oPYT5EjFgiQQMAADWzFlJfr/t+reVHsI8iRiwJAIGAABr6mCSf912/d8tPYT58RWrsAQCBgAAJEne1tTVb5cewXyIGDAwAQMAAL7hSJL/vKmrPyw9hHkQMWBAAgYAAHyTR5J8X1NXf1J6CNMnYsBABAwAADipB5Jc2dTV+0oPYdpEDBiAgAEAAKd1b5LXN3X1Z6WHMF0iBuyTgAEAADt2Z5LXNXX12dJDmCYRA/ZBwAAAgF27Oclrm7r6cukhTM/B0gNgqgQMAADYk0uTXNV2/YWlhzA9IgbsgYABAAD78qwk/6nt+ieUHsK0iBiwSwIGAAAM4kVJ/s+2688oPYTpEDFgFwQMAAAY1Pcl+ZXSI5gOD3vCDgkYAACwND/U1NXvlR7B+IkYsAMCBgAALNWDSb6rqasPlx7CuIkYcBoCBgAArMStSV7e1NWtpYcwXt7EgFMQMAAAYGUuSfL/tF1flR7CeIkYcBICBgAArNwrkvzLzX8Xh28iYsAJCBgAAFDMDyf5udIjGCd1C44jYAAAQHFHk7y5qat3lh7CuIgYsIWAAQAAo9EleUlTV58rPYTxcJ0ENm0GjN+IgAEAAGNQJ/m9tuvPKj2E8RAxINsCxk+X3gIAAHzDy5P8w9IjGA/XSVh7AgYAAIza0SSXN3X1rtJDKE/EYK0JGAAAMAm3JHlhU1d3lR5CWa6TsLYEDAAAmIwnJfnfN/8dnjUmYrCWBAwAAJicv5rkx0qPoCwVi7UjYAAAwGT1SV7W1NUnSw+hDCcxWCsCBgAATFqVja9dfUzpIZQhYrA2BAwAAJiFFyb5x6VHUIbrJKwFAQMAAGbnu5u6+pPSI1gtEYPZEzAAAGCWvpDk+U1dPVB6CKvjOgmzJmAAAMBsPS3J/1B6BKvlJAazJWAAAMDsPZrkJU1d3Vh6CKvhJAazJGAAAMBaODPJv2i73v+2XRP+i2Z2BAwAAFgrr0ryo6VHsBqukzArAgYAAKyle5N8R1NXt5cewnI5icFsCBgAALC2npjkN0uPYPmcxGAWBAwAACDJm5q6+k+lR7A8IgaTJ2AAAACbvpDk+U1dPVB6CMvhOgmTJmAAAABbPC3JPyg9guVxEoPJEjAAAIATeDTJc5q6+lzpIQzPSQwmScAAAABO4swkv1J6BMvhJAaTI2AAAAA78LKmrj5aegTDchKDSREwAACAHXp76QEMT8RgMgQMAABgF65ou/7K0iMYluskTIKAAQAA7MGfJnl5U1dHSg9hGE5iMHoCBgAAsEcvSfI3So9gOE5iMGoCBgAAsE83ZeMrVx8uPYT9cxKD0RIwAACAATw9yY+WHsEwnMRglAQMAABgQHcm+famru4rPYT9cRKD0REwAACAgV0Y//tiFpzEYFQEDAAAYEnuS/KUpq7uLj2EvXMSg9EQMAAAgCV6fJIfLz2C/XESg1EQMAAAgBW4Pcm3NXX1UOkh7I2TGBQnYAAAACtyUZIfKj2CvXMSg6IEDAAAYMU+leT5TV0dKT2E3XMSg2IEDAAAoIDnJPkrpUewNyIGRQgYAABAQT9XegB74zoJKydgAAAAI/CKpq4+XHoEu+MkBislYAAAACPxs6UHsHtOYrAyAgYAADAiR5I8s6mrm0oPYeecxGAlBAwAAGBkDib5+6VHsDtOYrB0AgYAADBSDyR5SlNXd5Uews44icFSCRgAAMCIPTbJD5cewc6JGCyNgAEAAEzA20oPYOdEDJZCwAAAACbihW3Xv7j0CHZGxGBwAgYAADAxTmNMhIc9GZSAAQAATFCb5JKmrh4qPYRTcxKDwQgYAADARDVJvq/0CE5PxGAQAgYAADBxrpRMgOsk7JuAAQAAzMCRJE9u6uq20kM4OScx2BcBAwAAmImDSX649AhOzUkM9kzAAAAAZuZTSZ7X1NXR0kM4MScx2BMBAwAAmKHnJHlF6RGcnIjBrgkYAADAjHngc8RcJ2FXBAwAAGDmvprk4qauDpcewjdzEoMdEzAAAIA1cEFcKRktEYMdETAAAIA18ubSAzgxEYPTEjAAAIA18/2lB3Bi3sTglAQMAABgTT2jqavPlx7Bdk5icFICBgAAsMacxhghEYMTEjAAAIA1512MEXKdhG8iYAAAAORwkguaurq79BAWnMRgGwEDAAAgSXJGkjeVHsF2IgbfIGAAAABs40rJyLhOQhIBAwAA4ATuTXJhU1cPlx7CBicxEDAAAABO7IlJXld6BAsixpoTMAAAAE7pe0sPYEHEWGMCBgAAwGldUXoAC97EWFMCBgAAwI5d3NTV7aVH4CTGWhIwAAAAduXy0gPYIGKsGQEDAABg164sPYANrpOsEQEDAABgT25JcmlTV0dLD1l3TmKsCQEDAABgz56U5DtKj0DEWAsCBgAAwL65UjICIsbMCRgAAACD8FWrI+BNjBkTMAAAAAbTJTmvqatHSg9ZZ05izJSAAQAAMKg6yStKj1h3IsYMCRgAAABL4V2MwkSMmREwAAAAlkbEKMybGDMiYAAAACzV4SRNU1dfLz1kXTmJMRMCBgAAwNKdkeQNpUesMxFjBgQMAACAlfFVqwWJGBMnYAAAAKyUdzEK8ibGhAkYAAAARVza1NVXSo9YR05iTJSAAQAAUIwrJYWIGBMkYAAAABTlSkkhrpNMjIABAABQ3J1JLmrq6mjpIevGSYwJETAAAABG4cIkf6n0iHUkYkyEgAEAADAqrpQUIGJMgIABAAAwOh73LMCbGCMnYAAAAIxSn+Tcpq4eKj1knTiJMWICBgAAwGhVSV5VesS6ETFGSsAAAAAYPe9irJiIMUICBgAAwCSIGCvmTYyRETAAAAAm42iS85q6urv0kHXhJMaICBgAAACTciDJZaVHrBMRYyQEDAAAgEnyVasrJGKMgIABAAAwWd7FWCFvYhQmYAAAAEze05u6+kLpEevASYyCBAwAAIBZcKVkRUSMQgQMAACA2XClZEVcJylAwAAAAJiVNskFTV0dKT1k7pzEWDEBAwAAYHaaJC8uPWIdiBgrJGAAAADMlislKyBirIiAAQAAMGse91wBb2KsgIABAAAwew8lObepq770kDlzEmPJBAwAAIC1cE6S15YeMXcixhIJGAAAAGvFuxhLJmIsiYABAACwdkSMJfMmxhIIGAAAAGvrwqauvlp6xFw5iTEwAQMAAGCtXV56wJyJGAMSMAAAANaer1pdItdJBiJgAAAAkOTLSb6tqaujpYfMkZMYAxAwAAAA2PSUJM8sPWKuRIx9EjAAAAA4jislSyJi7IOAAQAAwAn4qtUl8SbGHgkYAAAAnMS9Sc5v6urR0kPmxkmMPRAwAAAAOIUnJnlZ6RFzJGLskoABAADADrhSsgQixi4IGAAAAOyQxz2XwJsYOyRgAAAAsAuPJGmaurqv9JA5cRJjBwQMAAAAdumsJN9VesTciBinIWAAAACwR97FGJiIcQoCBgAAAPsgYgzMmxgnIWAAAAAwgEuaurqt9Ii5cBLjBAQMAAAABuJbSgYkYhxHwAAAAGBAIsaAXCfZQsAAAABgYLcleVJTV0dLD5kDJzE2CRgAAAAswcVJnlt6xFyIGBEwAAAAWCpXSgay9hFDwAAAAGDJfNXqQNb6TQwBAwAAgBW4P8m5TV09UnrI1K3tSQwBAwAAgBV5XJJXlh4xB2sZMQQMAAAAVsyVkgGsXcQQMAAAACjA454DWKs3MQQMAAAACjmc5Lymru4tPWTK1uYkhoABAABAQWckeUPpEVO3FhFDwAAAAGAEvIuxT7OPGJsB4+0RMAAAAChLxNin2UeMJL+Q5L8rPQIAAIC196y2659SesSUzTpitF3/E0l+rfQOAAAA2ORbSvZhthGj7fq3JHlH6R0AAACwhYixD7P8itW2678vyb/PxuuvAAAAMBZfTXJRU1dHSg+ZotmdxGi7/g1J/l0EDAAAAMbngiQvKD1iqmYVMdquf1mSP07ymNJbAAAA4CRcKdmj2USMtuufm+Q/Jnl86S0AAABwCr5qdY9m8SZG2/VPS3J9kktKbwEAAIDT6JM0TV09WHrI1Ez+JEbb9RcmuSoCBgAAANNQJXl16RFTNOmI0Xb92Un+MMm3l94CAAAAu+BKyR5MNmK0XX8gyTuSvKb0FgAAANglj3vuwWQjRpKfTPIjpUcAAADAHry07frzSo+YmklGjLbrL0/yW6V3AAAAwB4dSPLG0iOmZnIRo+36b0/y+0nOKL0FAAAA9sG7GLs0qYjRdn2d5D8kaUpvAQAAgH0SMXZpMhGj7fqDSf6PJM8rvQUAAAAG8LS2659eesSUTCZiJPlHSd5cegQAAAAMyGmMXZhExGi7/geT/FLpHQAAADAwX7W6CwdKDzidtutfkOQDSarSWwAAAGBgdye5oKmrw6WHTMGoT2K0Xf/YJP93BAwAAADm6dwkLyk9YipGHTGS/HqS55QeAQAAAEvkSskOjTZitF3//Un+TukdAAAAsGQe99yhUb6J0Xb9xUn+PMn5pbcAAADAkj2c5Nymrh4oPWTsRncSo+36g0l+OwIGAAAA6+HsJK8rPWIKRhcxkvxUkr9cegQAAACskCslOzCqiNF2/QuT/JPSOwAAAGDFPO65A6N5E6Pt+irJR5I8t/QWAAAAKOCipq7uKD1izMZ0EuPXI2AAAACwvi4rPWDsRhEx2q7/3iQ/WXoHAAAAFORdjNMofp2k7fo6yaeTXFJ6CwAAABT0lSRPaerqaOkhYzWGkxj/cwQMAAAAeHKSZ5UeMWZFI0bb9S9J8vdKbgAAAIARcaXkFIpFjLbrz0jyL0puAAAAgJHxVaunUDIg/DdJXlbw8wEAAGBs3th2/ZmlR4xVkYjRdv0lSX6txGcDAADAiD0hyctLjxirUicxfjMb/8UAAAAA23kX4yRWHjHarv/uJD+46s8FAACAiRAxTuLAKj+s7foqyceTPH2VnwsAAAAT8miSpqmrrvSQsVn1SYxfjIABAAAAp3JmkteXHjFGK4sYbdc/O8nPr+rzAAAAYMJ81eoJrPIkxq8mOWuFnwcAAABT5V2ME1jJmxht1788yYdW8VkAAAAwE09u6uqW0iPGZOknMdquP5Dk7cv+HAAAAJiZy0sPGJtVXCe5IsllK/gcAAAAmBNXSo6z1OskbdcfzMY1kpcu83MAAABghm5PcklTV0dLDxmLZZ/E+IEIGAAAALAXFyV5XukRY7K0iNF2/VnZ+EYSAAAAYG9cKdlimScx3prkmUv8+QAAADB3V5QeMCZLeROj7foqyeeSXLKMnw8AAABr4v4kTVNXD5ceMgbLOonx9yJgAAAAwH49LskrS48Yi8EjRtv1T0zyC0P/XAAAAFhT3sXYtIyTGD+e5Nwl/FwAAABYRyLGpkHfxGi7/uwkX4irJAAAADCUI0nOa+rqntJDShv6JMYPRsAAAACAIVNv7tEAACAASURBVB1M8sbSI8ZgsIjRdv2BJD8z1M8DAAAAvsFXrWbYkxiXJ3nhgD8PAAAA2OBdjAwbMZzCAAAAgOV4Ztv1Ty09orRBIkbb9c9P8qYhfhYAAABwQmt/pWSokxg/PdDPAQAAAE5s7a+U7PsrVtuuvzjJl5Kctf85AAAAwEl8Lcm3NnV1pPSQUoY4ifF3I2AAAADAsp2fNf9CjX1FjLbrH5fkJwbaAgAAAJzaWl8p2e9JjLckOXeIIQAAAMBprfXjnvuNGG8dYgQAAACwI69ru/4xpUeUsueI0Xb9s5K8esAtAAAAwKk9JslrSo8oZT8nMf72YCsAAACAnVrbdzH2FDHarj8jyd8aeAsAAABwemv7LsZeT2JcluTJQw4BAAAAduQlbdefV3pECXuNGG8bdAUAAACwUweSXF56RAm7jhht139Lkr+2hC0AAADAzqzllZK9nMT4G9l4DRUAAAAo48q26w+UHrFqe4kYbx16BAAAALAr35bk6aVHrNquIkbb9c9O8qolbQEAAAB2bu2+anW3JzH+9lJWAAAAALu1dhFjx/dnNu/afDbJM5Y3BwAAANihe5Kc39TV4dJDVmU3JzGeHQEDAAAAxuJbkry09IhV2k3EePPSVgAAAAB7sVZftbqbiPH9S1sBAAAA7MVavYuxozcx2q6/IMnt2dtXsgIAAADL8UiSc5u6ur/0kFXYaZT4nl38XgAAAGA1zkryutIjVmWnYcJVEgAAABintblSctqI0Xb9OUm+ewVbAAAAgN1bm8c9d3IS4w1JHr/kHQAAAMDevKDt+m8tPWIVdhIxfLUqAAAAjNtanMY4ZcRou/5AvIcBAAAAYydiJHlhkktXMQQAAADYsys3DyLM2ukixmUrWQEAAADsx5OSPLv0iGU7XcR47UpWAAAAAPs1+69aPWnE2DyGImIAAADANKxvxEjyzCQXrGoIAAAAsC9vaLv+rNIjlulUEeM1K1sBAAAA7Fed5BWlRyzTqSKGqyQAAAAwLbP+qlURAwAAAOZj1u9inPA7ZNuuvzDJHSveAgAAAOzP4SRNU1dfLz1kGU52EsN7GAAAADA9ZyR5fekRy3KyiOEqCQAAAEzTbK+UOIkBAAAA8zLbxz2/6U2Mtusfm+TeJGeufg4AAAAwgEubuvpK6RFDO9FJjBdGwAAAAIApm+VpjBNFjOeufAUAAAAwpLWJGM9b+QoAAABgSFe0Xf9NT0hMnZMYAAAAMD/fmuT5pUcMzUkMAAAAmKfZfdXqtojRdv0Tkjy50BYAAABgOLN7F+P4kxiukgAAAMA8vL7t+nNKjxjS8RHDVRIAAACYh8cmeVXpEUNyEgMAAADma1ZXSpzEAAAAgPma1eOeTmIAAADAfL2s7fpzS48YyjcixuY3k1xacAsAAAAwrINJ3lh6xFC2nsR4drEVAAAAwLLM5krJ1ojxpGIrAAAAgGWZzeOeWyPGxcVWAAAAAMvyjLbrv630iCFsjRiXFFsBAAAALNMsTmM4iQEAAADzN4t3MZzEAAAAgPm7vO36g6f/beMmYgAAAMD8nZfkRaVH7JfrJAAAALAeJn+l5GCStF1/VpILC28BAAAAlmfyj3seO4lxUdEVAAAAwLK9ru36qvSI/TgWMVwlAQAAgHk7J8lrS4/Yj2MRw6OeAAAAMH+TvlLiJAYAAACsj0k/7nksYnxL0RUAAADAKry47frzS4/Yq2MRY9IPewAAAAA7dnnpAXslYgAAAMB6meyVEhEDAAAA1suVbdcfKD1iL0QMAAAAWC9PSfKM0iP2QsQAAACA9TPJr1oVMQAAAGD9TPJdDBEDAAAA1s9lbdefWXrEbokYAAAAsH6emOSlpUfs1rGI8ZiiKwAAAIBVm9yVEicxAAAAYD1N7nFPEQMAAADW06vbrn986RG7cSxinF10BQAAALBqZyX5rtIjduNYxHik6AoAAACghEldKTkWMR4sugIAAAAoYVKPe4oYAAAAsL6e33b9RaVH7JSIAQAAAOttMldKRAwAAABYbyIGAAAAMAlXtl1/oPSInTgWMR4qugIAAAAo5ZIkzyk9YiecxAAAAAAmcaVExAAAAAAm8VWrIgYAAADwhrbrzyo94nREDAAAAODxSb6z9IjTETEAAACAZAJXSo5FjK8XXQEAAACUNvrHPY9FjNuLrgAAAABK+862659YesSpHIsYtxVdAQAAAJR2RpLXlx5xKiIGAAAAcMyo38VwnQQAAAA4ZhIR464kj5YcAgAAABT37LbrLy094mQOJklTV0fiNAYAAAAw4m8pObjlj0UMAAAAYBIRw+OeAAAAwBVt1x88/W9bPREDAAAA2OrCJH+p9IgTcZ0EAAAAON4or5Q4iQEAAAAcb5Rftbo1YtxabAUAAAAwJt/Vdv05pUccb2vE+FyxFQAAAMCYVEleXXrE8Y6PGI+WGgIAAACMyuiulHwjYjR19XCSvyi4BQAAABiP0T3uefz3vn6yyAoAAABgbF7Wdn1TesRWIgYAAABwIgeSvLH0iK1EDAAAAOBkRvUuxvER41NFVgAAAABjNOqI8dkkR0oMAQAAAEbn6W3XP730iGO2RYymrvokNxXaAgAAAIzPaL6l5PiTGIl3MQAAAIAFEQMAAACYhMvbrj+j9IhExAAAAABOrUny4tIjkhNHjBtXvgIAAAAYs1FcKTlRxPh4kvtWPQQAAAAYrVF81eo3RYymrh5N8v4CWwAAAIBxem3b9Y8tPeJEJzGS5L0rXQEAAACM2dlJXlt6xMkixvUrXQEAAACMXfF3MU4WMT6Y5PAqhwAAAACjVvxdjBNGjKau7kvysRVvAQAAAMbrRW3XX1hywMlOYiSulAAAAADbXVbyw08VMTzuCQAAAGxV9EqJiAEAAADs1JVt1x8o9eEnjRhNXd2a5Asr3AIAAACM26VJnlnqw091EiPxLgYAAACwXbErJSIGAAAAsBtXlPrg00WMa1cxAgAAAJiMy9quP7PEB58uYvxFki+vYggAAAAwCU9I8rISH3zKiNHU1dEkV61oCwAAADANRd7FON1JjCS5eukrAAAAgCkpEjFO+92ubddfkOTOFWwBAAAApuHRJOc2dXXfKj/0tCcxmrr6apKPrWALAAAAMA1nJnn9qj90J9dJEu9iAAAAANut/KtWdxoxvIsBAAAAbLXydzFO+yZGkrRdXyW5O8k5y50DAAAATMiTmrq6dVUftqOTGE1d9UmuX/IWAAAAYFouX+WH7fQ6SeJKCQAAALDdSq+U7CZieNwTAAAA2OqKtut39FTFEHYTMT6WpF3WEAAAAGByLk7y3FV92I4jRlNXh5McWuIWAAAAYHpWdqVkNycxEldKAAAAgO2uWNUH7TZieNwTAAAA2OoNbdefvYoP2lXEaOrqC0k+v6QtAAAAwPQ8Lsl3ruKDdnsSI3EaAwAAANhuJe9i7CVieBcDAAAA2GolEWPX3+Xadv25Se7ayz8LAAAAzNKRJE1TV/cu80N2fRKjqau7k3xkCVsAAACAaTqY5I2r+JC9cKUEAAAA2GrpX7W614jhcU8AAABgq6W/i7Gndy3arj8nSZvkscPOAQAAACbsqU1dfXlZP3xPJzGaunooyXUDbwEAAACmbalXSvZ6nSRxpQQAAADYbqlXSvYTMTzuCQAAAGx1edv1+2kNp7SfH/zxJHcONQQAAACYvAuSvGBZP3zPEaOpqyNxpQQAAADYbmnvYuz3iIeIAQAAAGy1tHcx9vQVq8e0Xf/kJDcPtAUAAACYvgeTnNvU1YND/+B9ncRo6uorST490BYAAABg+h6T5NXL+MFDvBjqSgkAAACw1VKulAwRMXzVKgAAALDVUh733NebGEnSdv0Tk9yV5Iz9zwEAAABm4GiSC5q6umvIH7rvkxhNXd2b5IMDbAEAAADm4UCSy4b+oUNcJ0lcKQEAAAC2G/xKyVARw+OeAAAAwFaDP+45VMT4YJL7BvpZAAAAwPQ9re36pw/5AweJGE1dPZLk2iF+FgAAADAbg57GGOokRuJdDAAAAGA7EQMAAACYhMvarj9jqB82ZMT4dJJbB/x5AAAAwLSdm+QlQ/2wwSJGU1dH4zQGAAAAsN1gX7U65EmMxFetAgAAANsN9i7GgaF+UJK0XX9RktuG/JkAAADApD2c5Nymrh7Y7w8a9CRGU1e3J7lxyJ8JAAAATNrZSV43xA8a+jpJ4koJAAAAsN0gV0qWETE87gkAAABsNcjjnoO+iZEkbdc/LsndSc4a+mcDAAAAk3VRU1d37OcHDH4So6mr+5O8b+ifCwAAAEza5fv9Acu4TpK4UgIAAABst+8rJcuKGB73BAAAALa6su36fT1rsayI8ZEk9y7pZwMAAADT8+Qkz9rPD1hKxGjq6nCSa5bxswEAAIDJ2tdXrS7rJEbiXQwAAABgu9FGDO9iAAAAAFu9se36M/f6Dy8zYnwuyZeW+PMBAACAaamTvGKv//DSIkZTV0fjSgkAAACw3Z6/anWZJzESV0oAAACA7fb8Lsa+vp/1dNquPz/JV5f5GQAAAMCkPJqkaeqq2+0/uNSTGE1dfS3JDcv8DAAAAGBSzkzy+r38g8u+TpJ4FwMAAADYbk9XSkQMAAAAYNX29LjnUt/ESJK266skdyc5Z9mfBQAAAEzGk5u6umU3/8DST2I0ddUnec+yPwcAAACYlMt3+w+s4jpJ4qtWAQAAgO12/S7GqiKGdzEAAACAra5ou35Xz1ysKmJ8LMldK/osAAAAYPwuSvK83fwDK4kYTV0dSXJoFZ8FAAAATMaurpSs6iRG4koJAAAAsN2uvmp1lRHD454AAADAVq9vu/7snf7mlUWMpq6+mORzq/o8AAAAYPQel+RVO/3NqzyJkbhSAgAAAGy34yslq44YrpQAAAAAW+34cc9dfR/rfrVdf26Sr2X18QQAAAAYpyNJzmvq6p7T/caVxoSmru5O8pFVfiYAAAAwageTvHGnv3HVvIsBAAAAbLWjKyUlIoZ3MQAAAICtdvS4Z4mI8f4kDxT4XAAAAGCcntl2/VNP95tWHjGaunooybtX/bkAAADAqJ32NEapbwlxpQQAAADY6rTvYpSKGB73BAAAALa6vO36U3aKUhHj40nuKPTZAAAAwPicn+T5p/oNRSJGU1dH40oJAAAAsN1rTvU3S53ESFwpAQAAALZ77an+ZsmI4SQGAAAAsNU4T2I0dXVLkk+V+nwAAABgdJ7adv2TT/Y3S57ESJzGAAAAALY76WmM0hHDuxgAAADAVid9F6N0xLg2yeHCGwAAAIDxGGfEaOqqS/KBkhsAAACAUXlB2/VPONHfKH0SI3GlBAAAAFg4mOSVJ/sbpXncEwAAANjqhI97jiFifChJV3oEAAAAMBonfBejeMRo6uqRJO8qvQMAAAAYjVe2XX/m8X+xeMTY5EoJAAAAcMxjkzzt+L84lojhcU8AAABgq+ce/xfGEjE+k+SW0iMAAACA0RhnxGjq6micxgAAAAAWnnf8XxhFxNgkYgAAAADHjPMkxqZDpQcAAAAAo/Edbddv6xajiRhNXd2R5M9L7wAAAABGoUry1K1/YTQRY5OvWgUAAACO2XalZGwRw7sYAAAAwDGjjhjvSfJw6REAAADAKIw3YjR1dX+S95XeAQAAAIzCeCPGJldKAAAAgCR5Ttv1B479yRgjhsc9AQAAgCSpk1x07E/GGDE+muSe0iMAAACAUbj42B+MLmI0dXU4yTWldwAAAACjMN6Iscm7GAAAAEAy8uskiYgBAAAAbBj9SYybknyx9AgAAACguHFHjKaujsZpDAAAAGDsEWOTr1oFAAAARv8mRpIcSnK09AgAAACgqPGfxGjq/7+9Ow/e7S7oO/7JngMcAgdZlX1HVllDWJPgWKt01Kpt3UpbtaViLVqlpda20hWRQW0HREesIijjVFmlTgiBsK8h7LssYfVAOMDJcpPbP57n8twkv3vvb3me53uW12vmN9wkv+XDDJCbN9/zPdXfJnln6R0AAABAUbdtu/6kZMARY8kjJQAAADBvZyQ5Kxl+xHC5JwAAAHDbZPgR4w1Jrig9AgAAAChq+BGjqasrkry+9A4AAACgqFE8TpJ4pAQAAADm7sxkHBHD5Z4AAAAwb6OJGJck+XLpEQAAAEAxZyQjiBhNXV2b5ILSOwAAAIBiRnMSI3EvBgAAAMyZiAEAAACMwngiRlNXn0rykdI7AAAAgCLGEzGWnMYAAACAeRpdxPCqVQAAAJincbyd5CgXJrm29AgAAABg68Z1EqOpq68meWvpHQAAAMDWjStiLHmkBAAAAOZndI+TJC73BAAAgDm6KhlfxHhzkm+UHgEAAABsVZ+MLGI0dXVVkotK7wAAAAC2anwRY8kjJQAAADAvo40YLvcEAACAeRltxHhfks+XHgEAAABszTgjRlNXh+M0BgAAAMzJOCPGknsxAAAAYD6uSMYbMZzEAAAAgPkY70mMpq4uS/L+0jsAAACArRhvxFjySAkAAADMw+gjhkdKAAAAYB5GHzEuSnKo9AgAAABg4y5PRhwxmrrqkryp9A4AAABg4z6XjDhiLHmkBAAAAKbv88n4I4bLPQEAAGDavtTU1VXJ+CPG25J8rfQIAAAAYGM+d+QXo44YTV0dSnJh6R0AAADAxlx25BejjhhLHikBAACA6ZpUxHC5JwAAAEzXNB4nWfpwkk+XHgEAAABsxHROYjR1dThOYwAAAMBUTeokRuJeDAAAAJiq6ZzEWLqg9AAAAABgI6YVMZq6+mKSS0rvAAAAANbqcJIvHPmDSUSMJY+UAAAAwLR8vKmrq478wZQihss9AQAAYFred/QfTClivD7JVSf8LAAAAGAs3n/0H0wmYjR19c0kF5feAQAAAKzNZE9iJB4pAQAAgCmZ5kmMJZd7AgAAwDQcTvLBo//E1CLGu5J8pfQIAAAA4MA+sbw64lsmFTGauromyQWldwAAAAAH9r7r/4lJRYwlj5QAAADA+M0iYrjcEwAAAMbv/df/E5OLGE1dfTzJx0vvAAAAAA5kFicxEqcxAAAAYMxu8GaSZLoRw70YAAAAMF4fuP6bSZLpRozXZFFtAAAAgPG5eKc/OcmI0dRVm+QdpXcAAAAA+zKfiLHkkRIAAAAYp9lFDJd7AgAAwPhcluSTO/2FKUeMNyTpS48AAAAA9uTipq52vOdyshGjqasrk7yu9A4AAABgT95wrL8w2Yix5JESAAAAGJcd78NIph8xXO4JAAAA4/H1JO851l+cesS4NMmXSo8AAAAAduVNTV0dOtZfnHTEaOrq2nikBAAAAMbimI+SJBOPGEseKQEAAIBxmH3EcBIDAAAAhu+KJG853idMPmI0dfXpJB8qvQMAAAA4rguauvrG8T5h8hFjyWkMAAAAGLaXnugT5hIx3IsBAAAAw/byE33CXCLGa5NcU3oEAAAAsKO3N3V12Yk+aRYRo6mry5O8tfQOAAAAYEcv280nzSJiLHmkBAAAAIbphPdhJPOKGC73BAAAgOH5dJJLdvOJc4oYb07y9dIjAAAAgOt4WVNXh3fzibOJGE1dXZ3kotI7AAAAgOvY1aMkyYwixpJ7MQAAAGA4vp7FG0V3RcQAAAAASnl1U1dX7vaT5xYxPpDkhO+dBQAAALbiz/fyybOKGMuLQrylBAAAAMq7PMlf7OULZhUxlkQMAAAAKO/FTV31e/kCEQMAAAAo4QV7/YLZRYymrj6X5L2ldwAAAMCMfSjJW/b6RbOLGEtOYwAAAEA5L1jeW7knc40YXrUKAAAAZVyb5I/284VzjRivS3J16REAAAAwQ3/d1NVn9/OFs4wYTV19PcmbSu8AAACAGXrBfr9wlhFjySMlAAAAsF2XJ/nL/X7xnCOGyz0BAABgu17c1FW/3y+ec8R4exYFCAAAANiOPzzIF882YjR1dSjJhaV3AAAAwEy8M8mbD/INZhsxltyLAQAAANvxrKauDh/kG4gYAAAAwKZ9OslLDvpN5h4xPprkU6VHAAAAwMQ9p6mrqw/6TWYdMZbHWJzGAAAAgM35WpLnr+MbzTpiLHnVKgAAAGzO85u6+to6vpGIkVxQegAAAABM1KEkz1nXN5t9xGjq6ktJ3lV6BwAAAEzQnzV19el1fbPZR4wlj5QAAADA+j1rnd9MxFhwuScAAACs14VNXb1znd9QxFi4OMmVpUcAAADAhKz1FEYiYiRJmrrqswgZAAAAwMFdmuRV6/6mIsaKR0oAAABgPf5dU1fXrvubihgrLvcEAACAg3tDklds4huLGCvvStKWHgEAAAAj97Smrg5v4huLGEvLYy4XlN4BAAAAI/bypq42duekiHFd7sUAAACA/Tmc5Omb/AEixnWJGAAAALA/L2zq6j2b/AEnbfKbj1Hb9R9NctfSOwAAAGBErk5yr6auPr7JH+Ikxg05jQEAAAB787xNB4xExNiJV60CAADA7n0jyTO28YNEjBt6TZJrS48AAACAkfjNpq6+sI0fJGJcT1NXX0ny9tI7AAAAYAS+mORZ2/phIsbOPFICAAAAJ/bUpq4u39YPEzF25nJPAAAAOL4LkvzJNn+gV6zuoO36M5K0SW5UegsAAAAM0FVJ7tfU1Ye3+UOdxNhBU1dXJnld6R0AAAAwUP9t2wEjETGOxyMlAAAAcEMfSfLfS/xgEePYXO4JAAAAN/Tkpq6uKPGDRYxjuzSLV8UAAAAAC3/S1FWx/9NfxDiGpq4Ox2kMAAAAOOLyJE8tOUDEOD73YgAAAMDC05q6+kLJASLG8TmJAQAAAMlbkvxu6REixnE0dfWZJB8svQMAAAAKujLJTzd1dW3pISLGiXmkBAAAgDn75aauLi09IhExdsMjJQAAAMzVq5L8dukRR5xUesDQtV1/0yRtklNKbwEAAIAt+lKS+5W+zPNoTmKcQFNXX0vy5tI7AAAAYMueNKSAkYgYu+WREgAAAObkd5q6ekXpEdcnYuyOyz0BAACYi/cm+eXSI3YiYuzOW5N0pUcAAADAhl2Z5B81ddWXHrITEWMXmrq6OslrS+8AAACADfuVobxOdScixu55pAQAAIAp+6skv1V6xPGIGLvnck8AAACm6gtZvI3kcOkhxyNi7N4Hk3y29AgAAABYs6uS/EBTV58vPeRERIxdWtYopzEAAACYmp9t6upNpUfshoixN+7FAAAAYEqe3dTVC0qP2C0RY2+cxAAAAGAq/jrJL5cesRcixh40dfWFJIN91QwAAADs0keT/GhTV4dKD9kLEWPvPFICAADAmHVJntjU1VdKD9krEWPvPFICAADAWB1O8g+buvpA6SH7IWLs3euyeP0MAAAAjM3Tmrp6RekR+yVi7FFTV99I8sbSOwAAAGCPXpjkmaVHHISIsT8eKQEAAGBM3pzkp5u6Olx6yEGIGPvjck8AAADG4tIk39vUVV96yEGJGPvzjiRfLT0CAAAATuCjSb57jG8i2YmIsQ9NXV2T5DWldwAAAMBxfDbJE5q6+nzpIesiYuyfR0oAAAAYqi9nETA+WXrIOokY++dyTwAAAIaoS/I9TV19oPSQdRMx9u9jST5ZegQAAAAc5Yok39fU1TtKD9kEEWOflq+lcRoDAACAoTiU5O83dfW60kM2RcQ4GPdiAAAAMASHk/xEU1evKD1kk0SMg3lNFv9BAQAAgJL+RVNXLy49YtNEjANo6urLSd5VegcAAACz9vNNXT2v9IhtEDEOziMlAAAAlPLzTV39dukR2yJiHJzLPQEAAChhVgEjETHW4eIsXmEDAAAA2zK7gJGIGAfW1NUVWYQMAAAA2IZZBoxExFgX92IAAACwDbMNGImIsS4iBgAAAJs264CRiBjrckmSL5ceAQAAwGTNPmAkIsZaNHV1bZILSu8AAABgkgSMJRFjfbxqFQAAgHUTMI4iYqyPezEAAABYJwHjekSMNWnq6m+SfKT0DgAAACZBwNiBiLFeHikBAADgoASMYxAx1ssjJQAAAByEgHEcIsZ6XZjk2tIjAAAAGCUB4wREjDVq6uqrSd5WegcAAACjI2Dsgoixfh4pAQAAYC8EjF0SMdbP5Z4AAADsloCxByLG+r0pyTdLjwAAAGDwBIw9EjHWrKmrq5JcVHoHAAAAgyZg7IOIsRnuxQAAAOBYBIx9EjE2Q8QAAABgJwLGAYgYm/G+JJ8vPQIAAIBBETAOSMTYgKauDsdbSgAAAFgRMNZAxNgcEQMAAIBEwFgbEWNzRAwAAAAEjDUSMTakqavPJnl/6R0AAAAUI2CsmYixWU5jAAAAzJOAsQEixmZ51SoAAMD8CBgbImJs1kVJDpUeAQAAwNYIGBskYmxQU1ddkjeX3gEAAMBWCBgbJmJsnkdKAAAApk/A2AIRY/Nc7gkAADBtAsaWiBib99YkXekRAAAAbISAsUUixoY1dXUoyYWldwAAALB2AsaWiRjb4V4MAACAaREwChAxtkPEAAAAmA4BoxARYzs+nOQzpUcAAABwYAJGQSLGFjR1dThOYwAAAIydgFGYiLE9XrUKAAAwXgLGAIgY2yNiAAAAjJOAMRAixpY0dfXFJJeU3gEAAMCeCBgDImJsl9MYAAAA4yFgDIyIsV0u9wQAABgHAWOARIzten2Sq0qPAAAA4LgEjIESMbaoqatvJnlD6R0AAAAck4AxYCLG9nmkBAAAYJgEjIETMbbP5Z4AAADDI2CMgIixfe9M8pXSIwAAAPgWAWMkRIwta+rqmiSvKb0DAACAJALGqIgYZbgXAwAAoDwBY2REjDJEDAAAgLIEjBE6qfSAuWq7/uNJ7lx6BwAAwAwJGCPlJEY5TmMAAABsn4AxYiJGOV61CgAAsF0CxsiJGOW8Jsnh0iMAAABmQsCYABGjkKau/jbJO0rvAAAAmAEBYyJEjLI8UgIAALBZAsaEiBhludwTAABgcwSMiRExynpjkitKjwAAAJggAWOCRIyCmrq6IsnrSu8AL2FKmAAAF6xJREFUAACYGAFjokSM8jxSAgAAsD4CxoSJGOW53BMAAGA9BIyJEzHKe0+SL5UeAQAAMHICxgyIGIU1dXVtkgtK7wAAABgxAWMmRIxhcC8GAADA/ggYMyJiDIN7MQAAAPZOwJgZEWMAmrr6VJIPl94BAAAwIgLGDIkYw+GREgAAgN0RMGZKxBgOj5QAAACcmIAxYyLGcFyY5JrSIwAAAAZMwJg5EWMgmrq6PMlbS+8AAAAYKAEDEWNgXll6AAAAwAAJGCQRMYbmZaUHAAAADIyAwbeIGMPyniSfKj0CAABgIAQMrkPEGJCmrg4neWnpHQAAAAMgYHADIsbwiBgAAMDcCRjsSMQYnouSdKVHAAAAFCJgcEwixsA0dXVVkr8qvQMAAKAAAYPjEjGGySMlAADA3AgYnJCIMUyvTHJN6REAAABbImCwKyLGADV11Sa5uPQOAACALRAw2DURY7heVnoAAADAhgkY7ImIMVzuxQAAAKZMwGDPRIyBaurqI0kuLb0DAABgAwQM9kXEGLYXlB4AAACwZgIG+3ZS6QEcW9v1t0ry2SSnlt4CAACwBgIGB+IkxoA1dfXFLF63CgAAMHZPETA4KBFj+P6g9AAAAIADekpTV79TegTj53GSgWu7/rQsHim5ZektAAAA+yBgsDZOYgxcU1dXJ/nj0jsAAAD2QcBgrUSMcXhB6QEAAAB7JGCwdiLGCDR19Z4k7yy9AwAAYJcEDDZCxBgPF3wCAABjIGCwMS72HIm265skn0tyeuktAAAAxyBgsFFOYoxEU1dtkr8svQMAAOAYBAw2TsQYl+eVHgAAALADAYOtEDHG5TVJ3l16BAAAwFEEDLZGxBiRpq4OJ3lm6R0AAABLAgZb5WLPkWm7/rQkH0ty+9JbAACAWRMw2DonMUamqaurkzy79A4AAGDWBAyKcBJjhNqur5N8OslZpbcAAACzI2BQjJMYI9TUVZfkuaV3AAAAsyNgUJSTGCPVdv3tknwyyWmFpwAAAPMgYFCckxgj1dTVZUleWHoHAAAwCwIGgyBijNtvlB4AAABMnoDBYIgYI9bU1fuSvKr0DgAAYLIEDAZFxBi/Z5YeAAAATJKAweCIGOP32iSvKT0CAACYFAGDQfJ2kglou/6hSd5aegcAADAJAgaD5STGBDR19bYkLym9AwAAGD0Bg0FzEmMi2q6/e5IPJDml9BYAAGCUBAwGz0mMiWjq6iNJnl96BwAAMEoCBqPgJMaEtF1/2yQfTXKj0lsAAIDREDAYDScxJqSpq88leXbpHQAAwGgIGIyKkxgT03b9WUk+luQWpbcAAACDJmAwOk5iTExTV5cneUbpHQAAwKAJGIySkxgT1Hb9GUk+lOSOpbcAAACDI2AwWk5iTFBTV1cm+dXSOwAAgMERMBg1EWO6XpjkjaVHAAAAgyFgMHoeJ5mwtuvvm+RdSU4tvQUAAChKwGASnMSYsKau3pvkmaV3AAAARQkYTIaTGBPXdv2Nklya5C6ltwAAAFsnYDApTmJMXFNX30zy5NI7AACArRMwmBwRYwaaunp1kheV3gEAAGyNgMEkeZxkJtquv02SDyS5WektAADARgkYTJaTGDPR1NXnk/xK6R0AAMBGCRhMmogxL7+X5I2lRwAAABshYDB5HieZmbbr75vkXUlOLb0FAABYGwGDWXASY2aaunpvkv9RegcAALA2Agaz4STGDLVdf3qSNyR5SOktAADAgQgYzIqIMVNt1989i8dKblx6CwAAsC8CBrPjcZKZaurqI0l+rvQOAABgXwQMZknEmLc/TPKnpUcAAAB7ImAwWx4nmbm262+W5JIkdyi9BQAAOCEBg1lzEmPmmrr6apIfS3Jt6S0AAMBxCRjMnohBmrq6OMmvl94BAAAck4AB8TgJS23Xn5rkoiSPLL0FAAC4DgEDlkQMvqXt+jsleXeSswpPAQAAFgQMOIrHSfiWpq4+meRnS+8AAACSCBhwAyIG19HU1Z8meVbpHQAAMHMCBuzA4yTcQNv1pyR5eZLvKb0FAABmSMCAYxAx2FHb9TdL8uYk9yy9BQAAZkTAgOMQMTimtuvvkeQtSW5WegsAAMyAgAEn4E4Mjqmpqw8n+dEk15beAgAAEydgwC6IGBxXU1f/L8lTS+8AAIAJEzBgl0QMduO3kvx+6REAADBBAgbsgTsx2JW2689IckGSc0pvAQCAiRAwYI9EDHat7fpbJXl7ktuX3gIAACMnYMA+eJyEXWvq6otJvj/J10pvAQCAERMwYJ9EDPakqatLknxfkitKbwEAgBESMOAARAz2rKmr1yf5wSSHSm8BAIARETDggEQM9qWpq1cl+bEkh0tvAQCAERAwYA1EDPatqas/S/IzpXcAAMDACRiwJiIGB9LU1e8l+aXSOwAAYKAEDFgjEYMDa+rqWUmeUXoHAAAMjIABayZisC7/IYn/gQYAgAUBAzbgpNIDmI62609O8oIkP1F4CgAAlCRgwIaIGKxV2/WnJnlxkh8qvQUAAAoQMGCDPE7CWjV1dSjJP0jyf0pvAQCALRMwYMNEDNZuGTKelOS3S28BAIAtETBgC0QMNqKpq2uT/Kskv156CwAAbJiAAVviTgw2ru36X0zyG6V3AADABggYsEUiBlvRdv0/S/K78Z85AACmQ8CALfMPlGxN2/U/kuSPk5xWegsAAByQgAEFiBhsVdv1fyfJnyepSm8BAIB9EjCgEBGDrWu7/tFJXp7kpqW3AADAHgkYUJC3k7B1TV29Psmjk/xN6S0AALAHAgYUJmJQRFNX70ny0CSvL70FAAB2QcCAARAxKKapqy8lOT/J80tvAQCA4xAwYCBEDIpq6uqqJD+b5ClJrik8BwAArk/AgAFxsSeD0Xb9eUlekuTmpbcAAEAEDBgcEYNBabv+bklemuTepbcAADBrAgYMkMdJGJSmrj6a5Owkryy9BQCA2RIwYKBEDAanqavLkzwxyf8svQUAgNkRMGDAPE7CoLVd/0NZvL3EPRkAAGyagAEDJ2IweG3X3z7JHyV5bOktAABMloABI+BxEgavqatPJzkvyb+P17ACALB+AgaMhJMYjErb9Y9I8idJ7lx6CwAAkyBgwIg4icGoNHX15iQPTPLC0lsAABg9AQNGxkkMRqvt+h9P8r+T1KW3AAAwOgIGjJCIwai1XX/XLE5lPLz0FgAARkPAgJHyOAmj1tTVx5I8Osl/THJV2TUAAIyAgAEj5iQGk9F2/b2SPDdexQoAwM4EDBg5JzGYjKauPpjk8UmelKQtPAcAgGERMGACnMRgktquv2WSZyb5qdJbAAAoTsCAiRAxmLS26x+fxSMm9yi9BQCAIgQMmBCPkzBpTV1dmOQBSf5TXPwJADA3AgZMjJMYzEbb9ffM4lTG4wpPAQBg8wQMmCAnMZiNpq4+lOTcJD+S5COF5wAAsDkCBkyUkxjMUtv1pyX5p0n+Y5Jbl10DAMAaCRgwYSIGs9Z2/Y2T/EKSX0lSF54DAMDBCBgwcSIGJGm7/tuSPD3Jk5OcXngOAAB7J2DADIgYcJS26++U5D8n+fH47wcAwFgIGDAT/iENdtB2/f2T/Nckf7f0FgAAjkvAgBkRMeA42q5/eJJfSvKD8TYfAIChETBgZkQM2IW26++a5KlJnpSkKjwHAAABA2ZJxIA9WF4A+uQkP5fkloXnAADMlYABMyViwD60XV8l+ckkv5jk7oXnAADMiYABMyZiwAG0XX9Kku9P8m+SPLLwHACAqRMwYOZEDFiTtuvPTvIzSX44yY0LzwEAmBoBAxAxYN3arr9JFiHjSUkeXXgOAMAUCBhAEhEDNqrt+rsl+anlx+0LzwEAGCMBA/gWEQO2YHl3xrlZnM74gSRnll0EADAKAgZwHSIGbFnb9TdL8qNJ/nGSR5RdAwAwWAIGcAMiBhTUdv23J/m+LN5wcn6SM8ouAgAYBAED2JGIAQPRdv2NkzwhyROzCBu3LLsIAKAIAQM4JhEDBmh5h8bDsggaT0xyn7KLAAC2QsAAjkvEgBFou/6uWTxy8r1ZvLbVxaAAwNQIGMAJiRgwMm3Xn5nknCwePTk/yXfFf5cBgHETMIBd8Q8+MHJt139bksdnETWekORORQcBAOyNgAHsmogBE9J2/UlJ7pLVKY3zktys6CgAgGMTMIA9ETFgwpYXhD44i6DxhCweQzmt6CgAgAUBA9gzEQNmZPka10dndVLj/mUXAQAzJWAA+yJiwIy1XX/rLB45OXKfxreXXQQAzICAAeybiAEk+dZ9GvfM6pTG45PURUcBAFMjYAAHImIAO2q7/rQkD8vqPo1HJDml6CgAYMwEDODARAxgV9quv2mSx2Z1UuPeZRcBACMiYABrIWIA+9J2/XdkdZ/G+UluXXYRADBQAgawNiIGcGDL+zTum1XQeGySGxUdBQAMgYABrJWIAaxd2/VnJDk7q/s0HpLk5KKjAIBtEzCAtRMxgI1ru/7mWbzt5EjUuFvZRQDAhgkYwEaIGMDWtV1/p6yCxnlJblF0EACwTgIGsDEiBlBU2/UnJ3lgVvdpPDrJGUVHAQD7JWAAGyViAIPSdn2V5JwsosYTkjyo7CIAYJcEDGDjRAxg0Nquv2WSc7N6/OSOZRcBADsQMICtEDGA0Vi+yvVuWQWNc5OcVXQUACBgAFsjYgCj1Xb9qUkenNV9Go9MclrRUQAwLwIGsFUiBjAZbdffJIuLQY/cp3HfsosAYNIEDGDrRAxgstquv00WJzSOPH5yu7KLAGAyBAygCBEDmIXlfRr3zipoPC7JTUpuAoCREjCAYkQMYJbarj8tycOzuk/j4UlOKToKAIZPwACKEjEAkrRdf1aSx2Z1n8Y9yy4CgMERMIDiRAyAHbRdf/us7tM4P8mtyi4CgKIEDGAQRAyAE2i7/uQs3nRy5JTGY5JURUcBwPYIGMBgiBgAe9R2/RlJHpnVfRoPif89BWCaBAxgUPymG+CA2q5vkjw+q6hx17KLAGAtBAxgcEQMgDVru/7OWQWN85I0ZRcBwJ4JGMAgiRgAG9R2/SlJHpjVfRqPSnJ60VEAcHwCBjBYIgbAFrVdf6MsQsb5WUSNB5ZdBADXIWAAgyZiABTUdv2tkpyb1UmN25ddBMCMCRjA4IkYAAPRdv1JSe6e1X0a5ya5adFRAMyFgAGMgogBMFBt15+axetbj5zSODvJqUVHATBFAgYwGiIGwEi0XX+TJI/N6j6N7yy7CIAJEDCAURExAEaq7frbZhU0zk9y27KLABgZAQMYHREDYAKW92ncJ6ug8bgkNy65CYBBEzCAURIxACao7frTkzw8q6jxsCSnFB0FwFAIGMBoiRgAM9B2/VlJHp/V4yf3KLsIgEIEDGDURAyAGWq7/g5ZBY3zktyy7CIAtkDAAEZPxACYubbrT05y/6yixmOSnFl0FADrJmAAkyBiAHAdbdefmeSRWQSNJyT5rvj7BcBYHU7y5Kaunlt6CMA6+E0pAMfVdv0tkpyb1UmNO5ddBMAuHUryk01dvaj0EIB1ETEA2JO26++S1SmNc5PcvOwiAHZwRZIfburq5aWHAKyTiAHAvrVdf0oWj5sceZXrOUlOLzoKgC7J9zd1dVHpIQDrJmIAsDZt1984yaOyihoPKLsIYHb+Nsn3NHX19tJDADZBxABgY9quv3UWr3A9cp/Gd5RdBDBplyV5QlNX7y89BGBTRAwAtqLt+pOS3COr+zQen6QuOgpgOj6WRcD4ROkhAJskYgBQRNv1pyZ5WFanNB6R5NSiowDG6b1Jvrupq8+VHgKwaSIGAIPQdn2d5LFZ3adxn7KLAEbhLUm+t6mrtvQQgG0QMQAYpLbrvz2LmHHk4zZlFwEMzkuSPKmpq2+UHgKwLSIGAIO3vE/jO7O6T+OxSW5UdBRAWb+a5L80dXW49BCAbRIxABidtutPT3J2VvdpPDTJyUVHAWzHN5L8RFNX/7f0EIASRAwARq/t+ptl8baTI/dp3L3sIoCN+ESSv9fU1aWlhwCUImIAMDlt198xq6BxfpJblF0EcGAXJvmRpq6+XHoIQEkiBgCT1nb9yUkekFXUeHSSM4uOAtib/5XkXzd1dXXpIQCliRgAzErb9VWSc7K6T+NB8fdDYJgOJfmXTV39bukhAEPhN20AzFrb9d+W5Nys3nxyx7KLAJIkX07yQ01dva70EIAhETEAYGn5Kte7ZnVK49wkNys6Cpiji7J4A8mnSw8BGBoRAwCOoe36U5I8OKv7NM5JclrRUcCUHUryq0me2dTVNaXHAAyRiAEAu9R2/Y2TPCarkxr3K7sImJCPJPmxpq7eVnoIwJCJGACwT23X3ybJeVndp3G7souAkfr9JL/Q1NXXSw8BGDoRAwDWYHmfxr2yOqXxuCR1yU3A4H0lyU83dfXnpYcAjIWIAQAb0Hb9aUkeltV9Go9IckrRUcCQXJjkJ5u6+kzpIQBjImIAwBa0XX/TLE5nHDmpca+ig4BSDiV5epJnubwTYO9EDAAooO3678giaBz5uHXZRcAWXJrknzR19fbSQwDGSsQAgMKW92ncL6tTGo9NUhUdBazTN5P8WpLnNHV1dekxAGMmYgDAwLRdf0aSs7O6T+MhSU4uOgrYr5cleUpTV39TegjAFIgYADBwbdffPMm5WZ3UuGvZRcAufCaLePEXpYcATImIAQAj03b9nbO6S+O8JLcouwg4yjVJnpPk15q6+nrpMQBTI2IAwIi1XX9ykgdldUrjUUnOKDoK5ustSf55U1fvLj0EYKpEDACYkLbrqyxCxpH7NB5UdhHMwuVJnpbk+V6bCrBZIgYATFjb9bfM4j6NJyw/7lB2EUzK1Umel+QZTV19ofQYgDkQMQBgJpavcr1bVqc0zk1yVtFRME6Hk7wwi3svPl56DMCciBgAMFNt15+axetbj9yncXaS04qOguF7eZKnN3X1ntJDAOZIxAAAkiRt198kyWOyOqlx37KLYFDekORpTV1dXHoIwJyJGADAjtquv20Wr3A9cp/GbcsugiIuTfJvk7yyqavDpccAzJ2IAQCc0PI+jXtndUrjcUluUnITbNgnk/xqkhd54wjAcIgYAMCetV1/epKHZ3WfxsOSnFJ0FKzHe5I8K8mLm7q6qvQYAK5LxAAADqzt+rOyOJ1x5KTGPYsOgr17dZLfSHKBx0YAhkvEAADWru36O2R1n8b5SW5ZdhHs6OosXpX6m01dXVp6DAAnJmIAABvVdv3JSe6XVdB4TJKq6Cjm7itJnpvkd5q6uqz0GAB2T8QAALaq7fozk5yd1VtPHhy/J2E7PpHk2Un+oKmrr5ceA8De+Q0DAFBU2/VNknOzuiT0LmUXMTGHkrwsyQuSvMKbRgDGTcQAAAal7fq7ZBU0zkty87KLGKl3ZREuXtTU1ZcKbwFgTUQMAGCw2q4/JcmDsrpP41FJTi86iiH7UpI/TvKHTV1dUnoMAOsnYgAAo9F2/Y2yCBlH7tN4QNlFDMDRj4u8qqmrq8vOAWCTRAwAYLTarr9VFo+cHHn85PZlF7Elh5O8KclLkrzQ4yIA8yFiAACT0Hb9SUnukVXQeHySmxYdxTp9I8mrszh18cqmrr5YeA8ABYgYAMAktV1/apKHZnWfxtlJTi06ir36bJKXLj9e29TVFYX3AFCYiAEAzELb9XWSx2QVNb6z7CKO4R1ZnLZ4aZJ3N3V1uPAeAAZExAAAZqnt+tsleVySc5Yf94/fG5Xw/iQXLz8ubOrqM4X3ADBg/kYNAJCk7fqzkjwii7efnLP8dVV01PRcleRtWUWLNzZ11ZadBMCYiBgAADtou/60JA/MKmo8Ksmti44an68keUNW0eId7rUA4CBEDACAXVi+/eTOSe6b5D5Hfdw7yY0KThuCK5J8IItHQ963/Hh/ko83dXVtyWEATIuIAQBwAG3Xn5zkDlkFjaMDx9Re8drnurHiyL9+sqmra0oOA2AeRAwAgA1Ynty4XZK7J7lNktsuP67/61uU2ng9lye5bPnxuR1+/dkknxIrAChJxAAAKKjt+jOyuGvj6LhRJzlz+XHGUb8+8xh//vQkV2ZxUuJ4H1cc9euv5KhQ0dTVNzf+bxYAAAAAAObg/wOrJ0h+1cXEDgAAAABJRU5ErkJggg=="/>
+        <linearGradient id="_Linear8" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(312.385,538.276,-538.276,312.385,693.479,500.292)"><stop offset="0" style="stop-color:rgb(145,129,238);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(111,141,251);stop-opacity:1"/></linearGradient>
+    </defs>
+</svg>
Binary file src/images/home/tk.png has changed
Binary file src/images/home/yt.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/applemusic.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,7 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
+<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-2.9 -2.9 63.80 63.80" xml:space="preserve" stroke="#000000" stroke-width="0.00058">
+
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
+
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
+
<g id="SVGRepo_iconCarrier"> <g> <g> <path d="M29,0C13.01,0,0,13.009,0,29s13.01,29,29,29s29-13.009,29-29S44.99,0,29,0z M29,56C14.112,56,2,43.888,2,29S14.112,2,29,2 s27,12.112,27,27S43.888,56,29,56z"/> <path d="M43.994,19.202c0.003-2.973,0.006-5.781-0.044-8.552c-0.016-0.811-0.519-1.325-1.313-1.343 c-0.478-0.015-0.953,0.02-1.43,0.054l-0.243,0.017c-2.592,0.17-5.127,0.353-7.657,0.723c-4.572,0.668-8.637,1.727-12.428,3.24 c-0.478,0.191-0.773,0.577-0.833,1.084c-0.042,0.354-0.044,0.708-0.044,1.087C20,22.293,19.998,29.074,20.004,35.878 c-2.247-0.426-4.352,0.055-6.277,1.433c-1.177,0.842-1.982,1.909-2.394,3.169c-0.649,1.978-0.356,3.749,0.872,5.264 c0.604,0.745,1.392,1.126,2.082,1.424c1.102,0.476,2.193,0.713,3.27,0.713c1.618,0,3.203-0.536,4.738-1.606 c1.705-1.188,2.632-2.906,2.681-4.969c0.023-0.987,0.024-1.974,0.025-2.961l0.002-0.848c0.005-1.553,0-3.106-0.004-4.66 c-0.009-2.822-0.018-5.74,0.036-8.614c0-0.017,0.001-0.034,0.001-0.05c0.016-0.004,0.032-0.009,0.05-0.014 c2.964-0.864,5.941-1.542,8.849-2.014c1.939-0.315,3.564-0.504,5.052-0.587c-0.006,3.426-0.007,6.851,0.001,10.276 c-0.346-0.07-0.715-0.121-1.102-0.132c-2.609-0.077-4.718,0.8-6.329,2.6c-1.217,1.36-1.729,2.967-1.479,4.647 c0.304,2.059,1.541,3.525,3.676,4.356c2.231,0.869,4.513,0.66,6.783-0.624v0C42.803,41.398,44,39.355,44,36.774V25.091 C43.99,22.996,43.992,21.052,43.994,19.202z M42,36.774c0,1.871-0.801,3.233-2.448,4.166c-1.746,0.986-3.404,1.152-5.072,0.501 c-1.457-0.567-2.227-1.453-2.424-2.786c-0.163-1.104,0.161-2.092,0.991-3.021c1.217-1.36,2.772-1.995,4.78-1.935 c0.544,0.016,1.088,0.158,1.548,0.294c0.167,0.049,0.676,0.2,1.143-0.147c0.216-0.161,0.473-0.475,0.473-1.056 c-0.012-3.982-0.011-7.964-0.002-11.945c0.002-0.411-0.123-0.74-0.371-0.981c-0.247-0.239-0.585-0.343-0.981-0.339 c-1.766,0.059-3.679,0.264-6.021,0.644c-2.988,0.485-6.046,1.181-9.097,2.071c-1.169,0.34-1.46,0.722-1.482,1.954 c-0.054,2.886-0.045,5.815-0.036,8.647c0.004,1.549,0.009,3.098,0.004,4.647l-0.002,0.852c-0.001,0.972-0.002,1.944-0.025,2.917 c-0.033,1.42-0.646,2.556-1.823,3.376c-1.979,1.378-3.967,1.607-6.073,0.697c-0.618-0.267-1.032-0.491-1.32-0.848 c-0.795-0.98-0.962-2.054-0.526-3.382c0.281-0.86,0.824-1.568,1.658-2.165c1.128-0.807,2.304-1.208,3.557-1.208 c0.628,0,1.274,0.1,1.944,0.3c0.169,0.051,0.687,0.204,1.149-0.141c0.464-0.346,0.464-0.887,0.464-1.066 C21.998,29.72,22,22.616,22.002,15.488c0-0.148-0.001-0.294,0.004-0.442c3.533-1.373,7.33-2.345,11.59-2.967 c2.457-0.359,4.949-0.539,7.5-0.706l0.25-0.017c0.205-0.014,0.41-0.029,0.615-0.039c0.038,2.549,0.036,5.146,0.033,7.883 c-0.002,1.853-0.004,3.801,0.006,5.896C42,25.096,42,36.774,42,36.774z"/> </g> </g> </g>
+
</svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/cashapp.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,7 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
+
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
+
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
+
<g id="SVGRepo_iconCarrier"> <circle cx="12" cy="12" r="10" stroke="#000000" stroke-width="0.8"/> <path d="M12 17V17.5V18" stroke="#000000" stroke-width="0.8" stroke-linecap="round"/> <path d="M12 6V6.5V7" stroke="#000000" stroke-width="0.8" stroke-linecap="round"/> <path d="M15 9.5C15 8.11929 13.6569 7 12 7C10.3431 7 9 8.11929 9 9.5C9 10.8807 10.3431 12 12 12C13.6569 12 15 13.1193 15 14.5C15 15.8807 13.6569 17 12 17C10.3431 17 9 15.8807 9 14.5" stroke="#000000" stroke-width="0.8" stroke-linecap="round"/> </g>
+
</svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/discord.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M18.8943 4.34399C17.5183 3.71467 16.057 3.256 14.5317 3C14.3396 3.33067 14.1263 3.77866 13.977 4.13067C12.3546 3.89599 10.7439 3.89599 9.14391 4.13067C8.99457 3.77866 8.77056 3.33067 8.58922 3C7.05325 3.256 5.59191 3.71467 4.22552 4.34399C1.46286 8.41865 0.716188 12.3973 1.08952 16.3226C2.92418 17.6559 4.69486 18.4666 6.4346 19C6.86126 18.424 7.24527 17.8053 7.57594 17.1546C6.9466 16.92 6.34927 16.632 5.77327 16.2906C5.9226 16.184 6.07194 16.0667 6.21061 15.9493C9.68793 17.5387 13.4543 17.5387 16.889 15.9493C17.0383 16.0667 17.177 16.184 17.3263 16.2906C16.7503 16.632 16.153 16.92 15.5236 17.1546C15.8543 17.8053 16.2383 18.424 16.665 19C18.4036 18.4666 20.185 17.6559 22.01 16.3226C22.4687 11.7787 21.2836 7.83202 18.8943 4.34399ZM8.05593 13.9013C7.01058 13.9013 6.15725 12.952 6.15725 11.7893C6.15725 10.6267 6.98925 9.67731 8.05593 9.67731C9.11191 9.67731 9.97588 10.6267 9.95454 11.7893C9.95454 12.952 9.11191 13.9013 8.05593 13.9013ZM15.065 13.9013C14.0196 13.9013 13.1652 12.952 13.1652 11.7893C13.1652 10.6267 13.9983 9.67731 15.065 9.67731C16.121 9.67731 16.985 10.6267 16.9636 11.7893C16.9636 12.952 16.1317 13.9013 15.065 13.9013Z" stroke="#000000" stroke-linejoin="round"/>
+</svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/email.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 48 48" id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;}</style></defs><path class="cls-1" d="M6.47,10.71a2,2,0,0,0-2,2h0V35.32a2,2,0,0,0,2,2H41.53a2,2,0,0,0,2-2h0V12.68a2,2,0,0,0-2-2H6.47Zm33.21,3.82L24,26.07,8.32,14.53" stroke-width="1.5"/></svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/facebook.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,2 @@
+<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Tools -->
+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><title/><path d="M44,7H20A13,13,0,0,0,7,20V44A13,13,0,0,0,20,57H44A13,13,0,0,0,57,44V20A13,13,0,0,0,44,7ZM33,55V38a1,1,0,0,0-1-1H27V31h5a1,1,0,0,0,1-1V22a5,5,0,0,1,5-5h8v6H42a3,3,0,0,0-3,3v4a1,1,0,0,0,1,1h6v6H40a1,1,0,0,0-1,1V55ZM55,44A11,11,0,0,1,44,55H41V39h6a1,1,0,0,0,1-1V30a1,1,0,0,0-1-1H41V26a1,1,0,0,1,1-1h5a1,1,0,0,0,1-1V16a1,1,0,0,0-1-1H38a7,7,0,0,0-7,7v7H26a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1h5V55H20A11,11,0,0,1,9,44V20A11,11,0,0,1,20,9H44A11,11,0,0,1,55,20Z"/></svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/instagram.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,2 @@
+<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Tools -->
+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><title/><path d="M44,57H20A13,13,0,0,1,7,44V20A13,13,0,0,1,20,7H44A13,13,0,0,1,57,20V44A13,13,0,0,1,44,57ZM20,9A11,11,0,0,0,9,20V44A11,11,0,0,0,20,55H44A11,11,0,0,0,55,44V20A11,11,0,0,0,44,9Z"/><path d="M32,43.67A11.67,11.67,0,1,1,43.67,32,11.68,11.68,0,0,1,32,43.67Zm0-21.33A9.67,9.67,0,1,0,41.67,32,9.68,9.68,0,0,0,32,22.33Z"/><path d="M44.5,21A3.5,3.5,0,1,1,48,17.5,3.5,3.5,0,0,1,44.5,21Zm0-5A1.5,1.5,0,1,0,46,17.5,1.5,1.5,0,0,0,44.5,16Z"/></svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/patreon.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,2 @@
+<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 21V4.13763H5.9653C5.89209 7.99504 7.25646 16.3342 13.5433 16.9773C19.1993 17.5559 21.3409 13.3403 20.9566 9.53798C20.7175 7.17319 18.8369 3.21186 12.7745 4.13763C6.71212 5.06341 6.29478 10.8605 6.29478 14.4975V21" stroke="#000000" stroke-linecap="round" stroke-linejoin="round"/></svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/paypal.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" stroke-width="3" stroke="#000000" fill="none"><path d="M20.55,49.27H12.81A1.81,1.81,0,0,1,11,47.17L17,11a1.79,1.79,0,0,1,1.77-1.5H38.5s11.07,0,10,11.13"/><path d="M20.77,52.45,25.91,21.3a1.81,1.81,0,0,1,1.78-1.51h15.2S54.52,19,52.82,31.05c-1.35,9.63-9.3,11.45-11.53,11.58-1.69.1-4.9,0-6.37,0a.89.89,0,0,0-.9.74L32.17,53.81a.9.9,0,0,1-.89.74H22.54A1.79,1.79,0,0,1,20.77,52.45Z"/></svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/pinterest.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 
+	 viewBox="0 0 512 512" xml:space="preserve">
+<g>
+	<g>
+		<path d="M437.019,74.981C388.667,26.629,324.38,0,256,0S123.333,26.629,74.981,74.981C26.628,123.333,0,187.62,0,256
+			c0,29.27,4.902,57.98,14.569,85.332c1.475,4.174,6.055,6.362,10.229,4.887c4.174-1.475,6.362-6.055,4.887-10.229
+			c-9.059-25.632-13.652-52.545-13.652-79.99C16.033,123.682,123.682,16.033,256,16.033S495.967,123.682,495.967,256
+			S388.318,495.967,256,495.967c-87.179,0-167.639-47.405-209.98-123.716c-2.147-3.87-7.028-5.27-10.899-3.12
+			c-3.872,2.148-5.269,7.028-3.12,10.899C77.167,461.432,162.998,512,256,512c68.38,0,132.667-26.629,181.019-74.981
+			C485.372,388.667,512,324.38,512,256S485.372,123.333,437.019,74.981z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M388.479,128.005c-22.171-28.818-54.738-49.677-91.7-58.735c-36.387-8.916-72.84-7.111-105.414,5.223
+			c-30.013,11.367-56.065,31.218-75.335,57.408c-39.104,53.138-43.344,122.371-10.8,176.38c8.16,13.544,25.818,17.925,39.371,9.765
+			c13.545-8.163,17.925-25.824,9.763-39.371c-20.544-34.096-17.456-78.363,7.868-112.776
+			c27.535-37.419,72.727-52.714,120.895-40.913c42.594,10.437,85.068,47.868,77.671,105.391c-3.694,28.73-16.325,54.7-35.567,73.124
+			c-15.878,15.206-34.401,23.285-49.549,21.607c-12.778-1.416-23.321-5.18-31.421-11.204c8.448-26.874,14.829-49.913,18.979-68.541
+			c8.187-36.741,8.248-56.645,0.218-70.991c-5.318-9.495-13.971-15.76-24.369-17.64c-10.185-1.843-29.957-1.605-47.943,20.653
+			c-11.881,14.701-20.676,37.096-24.127,61.44c-3.777,26.635-0.828,53.528,8.306,75.728c1.491,3.624,3.161,7.161,4.979,10.555
+			c-17.005,50.808-34.438,95.682-34.615,96.139c-2.779,7.14-2.61,14.935,0.475,21.948c3.086,7.013,8.716,12.405,15.855,15.182
+			c3.336,1.3,6.831,1.958,10.383,1.958c0.003,0,0.007,0,0.012-0.002c11.919,0,22.414-7.179,26.748-18.322
+			c0.55-1.421,12.767-32.971,26.568-72.485c12.986,6.592,27.616,10.818,43.629,12.594c32.298,3.585,67.119-9.97,95.545-37.188
+			c28.678-27.462,47.426-65.547,52.787-107.24C422.861,197.496,412.76,159.567,388.479,128.005z M401.789,235.651
+			c-4.896,38.07-21.934,72.769-47.974,97.704c-24.987,23.925-55.124,35.888-82.689,32.833c-17.27-1.915-32.598-7.013-45.557-15.154
+			c-2.071-1.302-4.624-1.585-6.931-0.772c-2.307,0.813-4.116,2.636-4.913,4.949c-14.942,43.375-28.924,79.489-29.521,81.024
+			c-1.906,4.9-6.534,8.066-11.795,8.066c-0.002,0-0.004,0-0.005,0c-1.557,0-3.094-0.291-4.572-0.866
+			c-3.148-1.225-5.632-3.603-6.993-6.696c-1.36-3.093-1.434-6.53-0.209-9.678c0.184-0.471,18.527-47.694,36.004-100.238
+			c0.725-2.177,0.48-4.562-0.671-6.548c-2.162-3.732-4.117-7.711-5.813-11.826c-8.063-19.596-10.64-43.523-7.258-67.373
+			c3.059-21.56,10.612-41.101,20.724-53.615c10.183-12.601,20.586-15.381,27.781-15.381c1.843,0,3.475,0.183,4.838,0.431
+			c5.598,1.012,10.298,4.456,13.231,9.695c5.725,10.227,5.164,28.072-1.877,59.671c-4.311,19.343-11.119,43.67-20.234,72.303
+			c-0.942,2.958-0.091,6.193,2.184,8.306c11.067,10.276,25.997,16.52,44.376,18.558c19.809,2.193,43.136-7.513,62.404-25.962
+			c21.879-20.951,36.219-50.306,40.38-82.66c8.653-67.285-40.462-110.931-89.757-123.009
+			c-54.674-13.393-106.121,4.17-137.625,46.983c-29.175,39.647-32.584,90.893-8.687,130.554c3.6,5.974,1.669,13.763-4.303,17.362
+			c-5.979,3.598-13.766,1.665-17.363-4.305c-29.19-48.443-25.273-110.699,9.98-158.604c17.435-23.695,40.982-41.647,68.098-51.916
+			c29.528-11.18,62.697-12.786,95.92-4.643c33.44,8.194,62.849,26.995,82.809,52.938
+			C397.407,165.909,406.404,199.753,401.789,235.651z"/>
+	</g>
+</g>
+</svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/reddit.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Tools -->
+<svg width="800px" height="800px" viewBox="0 0 17 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g>
+</g>
+	<path d="M15.581 9.936c0.024 0.161 0.040 0.33 0.040 0.491 0 1.308-0.766 2.517-2.145 3.42-1.347 0.879-3.121 1.363-5.008 1.363s-3.669-0.484-5.008-1.363c-1.388-0.903-2.145-2.112-2.145-3.42 0-0.177 0.016-0.354 0.040-0.532-0.508-0.322-0.855-0.895-0.855-1.541 0-1.007 0.815-1.822 1.822-1.822 0.452 0 0.872 0.17 1.194 0.444 1.291-0.823 2.976-1.291 4.774-1.324l1.081-3.41c0.048-0.153 0.21-0.242 0.371-0.21l2.799 0.661c0.233-0.532 0.766-0.903 1.379-0.903 0.831 0 1.5 0.678 1.5 1.501 0 0.83-0.669 1.508-1.5 1.508-0.823 0-1.492-0.67-1.5-1.492l-2.541-0.597-0.935 2.951c1.701 0.072 3.29 0.541 4.516 1.339 0.322-0.29 0.75-0.468 1.218-0.468 1.007 0 1.822 0.815 1.822 1.822 0 0.678-0.371 1.267-0.919 1.582zM1.532 9.25c0.258-0.693 0.75-1.339 1.451-1.896-0.184-0.128-0.419-0.201-0.661-0.201-0.661 0-1.201 0.54-1.201 1.201 0 0.356 0.161 0.678 0.411 0.896zM15 10.427c0-1.080-0.662-2.112-1.863-2.896-1.242-0.806-2.903-1.257-4.669-1.257s-3.428 0.452-4.67 1.257c-1.202 0.783-1.863 1.815-1.863 2.896 0 1.089 0.661 2.121 1.863 2.904 1.242 0.806 2.903 1.258 4.669 1.258s3.428-0.452 4.669-1.258c1.202-0.783 1.864-1.815 1.864-2.904zM6.097 10.661c-0.605 0-1.121-0.492-1.121-1.097 0-0.612 0.516-1.121 1.121-1.121s1.105 0.509 1.105 1.121c0 0.605-0.5 1.097-1.105 1.097zM11.081 12.267c0.121 0.12 0.121 0.322 0 0.443-0.54 0.54-1.379 0.798-2.573 0.798h-0.016c-1.194 0-2.033-0.258-2.573-0.798-0.121-0.121-0.121-0.323 0-0.443 0.121-0.122 0.314-0.122 0.436 0 0.419 0.419 1.113 0.62 2.137 0.62h0.016c1.017 0 1.718-0.201 2.137-0.62 0.121-0.122 0.314-0.122 0.436 0zM12.024 9.564c0 0.604-0.5 1.097-1.105 1.097s-1.121-0.492-1.121-1.097c0-0.612 0.516-1.121 1.121-1.121s1.105 0.509 1.105 1.121zM13.040 3.291c0 0.482 0.395 0.878 0.879 0.878s0.879-0.396 0.879-0.878c0-0.484-0.396-0.88-0.879-0.88-0.484 0-0.879 0.396-0.879 0.88zM15.879 8.354c0-0.661-0.54-1.201-1.201-1.201-0.258 0-0.5 0.081-0.694 0.226 0.694 0.557 1.185 1.21 1.436 1.92 0.282-0.227 0.459-0.566 0.459-0.945z" fill="#000000" />
+</svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/snapchat.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,2 @@
+<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><title/><path d="M32,60a16.3,16.3,0,0,1-10.71-4.29c-1.08-1.08-2.05-1-4-.85A23,23,0,0,1,15,55c-2.54,0-3.88-1.23-4-3.65a27,27,0,0,0-3.36-1.41C4.34,48.7,3,48.15,3,47s1-1.44,2.73-2.08c2.19-.83,5.51-2.07,7.6-4.78a13.53,13.53,0,0,0,2.39-5.86A38.77,38.77,0,0,1,9.93,32a3,3,0,0,1,2.74-5.34A32,32,0,0,0,16,28V21a16,16,0,0,1,32,0v7a32.14,32.14,0,0,0,3.33-1.38A3,3,0,0,1,54.07,32a38.77,38.77,0,0,1-5.8,2.28,13.53,13.53,0,0,0,2.39,5.86c2.09,2.71,5.4,4,7.6,4.78C60,45.56,61,46,61,47s-1.34,1.7-4.65,2.94A27,27,0,0,0,53,51.35C52.88,53.77,51.54,55,49,55a23,23,0,0,1-2.31-.14c-1.92-.19-2.9-.23-4,.85A16.3,16.3,0,0,1,32,60ZM18.88,52.75a5,5,0,0,1,3.83,1.54A14.35,14.35,0,0,0,32,58a14.35,14.35,0,0,0,9.29-3.71c1.8-1.8,3.64-1.62,5.59-1.43.67.07,1.36.13,2.12.13,2,0,2-1,2-2s1.34-1.7,4.65-2.94c.79-.29,1.83-.68,2.58-1l-.67-.25c-2.26-.85-6-2.27-8.48-5.43h0a15.88,15.88,0,0,1-2.92-7.66,1,1,0,0,1,.71-1.09,38.19,38.19,0,0,0,6.28-2.39,1,1,0,0,0-.92-1.78,36,36,0,0,1-4.94,1.95,1,1,0,0,1-1.3-1V21a14,14,0,0,0-28,0v8.43a1,1,0,0,1-1.3,1,35.93,35.93,0,0,1-4.93-1.94,1,1,0,0,0-1.36.43,1,1,0,0,0,.43,1.35,38.19,38.19,0,0,0,6.28,2.39,1,1,0,0,1,.71,1.09,15.88,15.88,0,0,1-2.92,7.66c-2.44,3.16-6.22,4.58-8.48,5.43L5.77,47c.75.33,1.79.72,2.58,1C11.66,49.3,13,49.85,13,51s0,2,2,2c.75,0,1.45-.07,2.12-.13S18.3,52.75,18.88,52.75Zm40.31-5.26h0Z"/></svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/soundcloud.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Tools -->
+<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 
+	 viewBox="0 0 511.999 511.999" xml:space="preserve">
+<g>
+	<g>
+		<path d="M10.199,250.09C4.566,250.09,0,254.656,0,260.289v97.121c0,5.633,4.566,10.199,10.199,10.199
+			c5.633,0,10.199-4.566,10.199-10.199v-97.121C20.398,254.656,15.832,250.09,10.199,250.09z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M49.766,238.916c-5.633,0-10.199,4.566-10.199,10.199v118.663c0,5.633,4.566,10.199,10.199,10.199
+			s10.199-4.566,10.199-10.2V249.115C59.965,243.482,55.399,238.916,49.766,238.916z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M89.975,208.882c-5.633,0-10.199,4.566-10.199,10.199v150.066c0,5.633,4.566,10.199,10.199,10.199
+			c5.633,0,10.199-4.566,10.199-10.198V219.081C100.175,213.448,95.608,208.882,89.975,208.882z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M130.86,179.397c-5.633,0-10.199,4.566-10.199,10.199v179.533c0,5.633,4.566,10.199,10.199,10.199
+			s10.199-4.566,10.199-10.199V189.597C141.059,183.964,136.493,179.397,130.86,179.397z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M172.388,177.685c-5.633,0-10.199,4.566-10.199,10.199v181.3c0,5.633,4.566,10.199,10.199,10.199
+			c5.633,0,10.199-4.566,10.199-10.2V187.884C182.587,182.251,178.021,177.685,172.388,177.685z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M214.539,159.829c-5.633,0-10.199,4.566-10.199,10.199v199.361c0,5.633,4.566,10.199,10.199,10.199
+			c5.633,0,10.199-4.566,10.199-10.2v-199.36C224.738,164.395,220.172,159.829,214.539,159.829z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M438.257,230.703c-5.483,0-10.88,0.602-16.146,1.795c-4.906-25.833-17.856-49.457-37.216-67.513
+			c-22.626-21.1-52.055-32.721-82.865-32.721c-14.987,0-30.32,2.904-44.335,8.395c-3.089,1.207-12.488,4.881-12.61,16.958
+			c0,0.035,0,0.068,0,0.103v204.106c0,0.048,0.001,0.095,0.001,0.143c0.128,9.117,7.019,16.69,16.029,17.614
+			c0.042,0.005,0.084,0.009,0.126,0.012c1.078,0.097,175.262,0.138,177.017,0.138c40.661,0,73.742-33.43,73.741-74.522
+			C511.999,264.127,478.919,230.703,438.257,230.703z M438.258,359.337c-1.478,0-148.84-0.07-172.776-0.092h0.002V159.519
+			c11.579-4.486,24.207-6.855,36.547-6.855c53.2,0,96.831,40.465,101.487,94.126c0.281,3.257,2.108,6.181,4.91,7.863
+			c2.802,1.682,6.24,1.919,9.248,0.638c6.524-2.779,13.45-4.189,20.582-4.189c29.413,0,53.344,24.274,53.344,54.11
+			C491.602,335.057,467.671,359.337,438.258,359.337z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M394.709,329.434H288.637c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h106.072
+			c5.633,0,10.199-4.566,10.199-10.199C404.908,334,400.342,329.434,394.709,329.434z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M436.526,329.434h-5.1c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h5.1
+			c5.633,0,10.199-4.566,10.199-10.199C446.725,334,442.159,329.434,436.526,329.434z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/spotify.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg viewBox="0 0 24 24" fill="currenColor" xmlns="http://www.w3.org/2000/svg">
+<path d="M18,10.561a.494.494,0,0,1-.245-.065,15.2,15.2,0,0,0-10.95-1.55.5.5,0,0,1-.232-.973A16.2,16.2,0,0,1,18.25,9.626a.5.5,0,0,1-.247.935Z"></path><path d="M16.646,13.632a.5.5,0,0,1-.249-.066,12.459,12.459,0,0,0-9.121-1.292.5.5,0,1,1-.237-.971A13.458,13.458,0,0,1,16.9,12.7a.5.5,0,0,1-.25.933Z"></path><path d="M15.312,16.583a.5.5,0,0,1-.251-.068,9.777,9.777,0,0,0-7.295-1.033.5.5,0,0,1-.245-.97,10.768,10.768,0,0,1,8.043,1.139.5.5,0,0,1-.252.932Z"></path>
+<path d="M12,23A11,11,0,1,1,23,12,11.013,11.013,0,0,1,12,23ZM12,2A10,10,0,1,0,22,12,10.011,10.011,0,0,0,12,2Z"></path>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/threads.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="616.19" height="712.02" viewBox="0 0 616.19 712.02"><path d="M801.51,364.61c-24-80.27-68.56-144.35-145.26-182.21C595,152.17,529.48,145.27,462.41,152.8c-115.48,13-196.73,72.39-236.12,182.69C189.24,439.19,189.43,544.9,221,650c20.74,69.17,60.59,125.05,124.43,161.51C395.57,840.21,450.47,850,531.66,850c57.75.26,130.68-21.51,189.16-82.54,78.42-81.83,77.2-232.31-51.36-292.6-7.5-3.5-9.85-7.73-10.43-15.48C657.29,435.8,652,413,641.61,391.55c-26-53.8-71.39-76.62-128.82-78.11-52.23-1.36-96.63,16-129.34,58.64-4.12,5.36-3.78,8.07,1.77,11.65,12,7.73,23.88,15.6,35.23,24.19,6.41,4.85,9.64,3.8,14.77-2.13,26.44-30.56,61-35,98.14-28.07,35.34,6.61,53.36,30.61,61.89,63.94,2.4,9.39-.36,10.55-8.45,9.29-27.93-4.37-56-4.32-84.14-2.13-28.44,2.22-55.22,10-79.55,25.3-53.8,33.72-69.1,104.68-32.74,156.51,24.53,35,60.62,50.26,102,52.94,56.86,3.68,105-13.21,137.33-63.24,14.23-22,21.87-46.62,25.92-72.35,1.19-7.53,2.63-10.08,10.27-4.67,37.25,26.4,54.93,71,45.08,115.73-11.79,53.54-47.38,87-94.85,109-46.08,21.26-95.41,23.57-145,18.42-95.6-9.88-161.36-58.86-191-151.45-27.78-86.74-27.77-174.6-2.54-261.82C299.33,297.8,346,244.57,423.49,223c57.73-16,115.9-15.4,173.32,2.06,53.11,16.14,93.85,48.43,121.18,97.18a277.05,277.05,0,0,1,24.13,57.32c1.36,4.61,2.25,7.73,8.32,6,15.17-4.41,30.45-8.4,45.75-12.26C801.22,372,803.23,370.38,801.51,364.61ZM598.24,522.44c-3,21.41-6.78,42.5-17.82,61.6-10.35,17.88-25.21,29.87-45.34,34.76-24.6,6-49,5.65-72.22-5.3-17.78-8.37-29.9-21.6-31-42.39-1.08-21.06,9.12-36,26.45-46.58,19-11.59,40.44-14,62.06-15.15,5.37-.29,10.77-.05,16.17-.05,18.15-.5,36.06,1.95,54,4.51C595.72,514.57,599.16,515.85,598.24,522.44Z" transform="translate(-191.92 -143.98)" fill-opacity="0"/><path d="M532.58,856h-1.1c-85.48,0-140.26-11.38-189.07-39.26-62.24-35.55-105-91.06-127.19-165-32.79-109.33-31-216.42,5.43-318.28C260,223.19,341.15,160.4,461.75,146.85c74.55-8.37,139,1.5,197.15,30.18,73.27,36.16,121.8,97,148.35,185.87h0c.8,2.7,1.62,6.71-.47,10.39s-5.77,5-9.13,5.82c-13.44,3.39-29.36,7.49-45.54,12.2-11.78,3.42-14.54-5.94-15.72-9.94a273.17,273.17,0,0,0-23.63-56.16c-26.28-46.87-65.89-78.63-117.7-94.38-56.14-17.07-113.33-17.75-170-2-71.85,20-119.55,69.08-141.79,146-25.27,87.37-24.43,174.28,2.5,258.33C314,721,376.51,770.61,471.72,780.46c57.44,5.95,102.52.26,141.9-17.91,51.25-23.65,81.19-57.93,91.51-104.8,9.32-42.3-7-84.28-42.69-109.55l-.69-.48c-.06.35-.13.75-.2,1.19-4.65,29.53-13.42,54-26.81,74.67-16.15,25-37,43.18-62.11,54-23,9.91-50.08,13.93-80.63,11.95-47-3-82.84-21.69-106.53-55.47a113.8,113.8,0,0,1-18.6-88.74A116.69,116.69,0,0,1,419.93,469c23.9-15,51.57-23.8,82.27-26.19C534.8,440.31,562,441,587.73,445c.91.14,1.63.23,2.2.28-.1-.56-.25-1.27-.48-2.16-9-35-27.14-53.93-57.19-59.55-41.08-7.66-70.47.64-92.51,26.11-5.26,6.07-12.09,11.17-22.91,3-11.69-8.84-24.13-17-34.87-23.94-2.92-1.88-6.37-4.62-7.14-9.06s1.65-8.39,3.86-11.27c32.18-41.94,77.37-62.47,134.26-61,63.59,1.65,108.7,29.07,134,81.5,10,20.77,16.1,44.32,18,70,.45,6,1.82,8.09,7,10.51,56.88,26.68,94.13,73.19,104.88,131,11.27,60.53-8.55,126.13-51.74,171.19C652.41,847.53,567,856,532.58,856Zm-.92-12h.91c32.82,0,114.37-8.09,183.92-80.69,40.57-42.33,59.2-103.91,48.62-160.72-10-53.88-44.91-97.32-98.2-122.31-9.07-4.25-13.09-10.18-13.86-20.47-1.81-24.51-7.31-46-16.83-65.67-23.58-48.74-64-73.19-123.58-74.73-53.63-1.41-94.32,17-124.44,56.29a16.44,16.44,0,0,0-1.27,1.86,13.12,13.12,0,0,0,1.54,1.12c10.93,7.07,23.59,15.36,35.6,24.45,2.37,1.79,3.24,1.85,3.25,1.85s.93-.31,3.37-3.13c24.76-28.62,58.71-38.45,103.77-30,34.69,6.49,56.48,28.84,66.6,68.35,1.21,4.76,2,10.14-1.5,14-3.86,4.24-10.25,3.25-13.68,2.71-24.81-3.87-51.1-4.53-82.75-2.07-28.75,2.24-54.6,10.45-76.83,24.4a104.75,104.75,0,0,0-47.69,68.51,101.93,101.93,0,0,0,16.66,79.49c21.5,30.66,54.31,47.61,97.5,50.4,60,3.88,103.11-15.91,131.91-60.51,12.47-19.29,20.66-42.19,25-70,.61-3.9,1.54-9.8,6.62-11.94,4.61-1.94,9.17.58,13,3.31,39.66,28.11,57.85,74.82,47.47,121.91-11.19,50.77-43.3,87.77-98.19,113.1-41.41,19.11-88.49,25.13-148.15,18.94C370.11,782,304.13,729.64,274.39,636.78c-27.66-86.35-28.54-175.62-2.6-265.31,23.47-81.17,74-133.05,150.1-154.2,58.88-16.35,118.31-15.64,176.66,2.1,54.88,16.68,96.82,50.32,124.66,100a284.51,284.51,0,0,1,24.65,58.55c.19.65.41,1.39.62,2l.29-.08c16.37-4.76,32.42-8.9,46-12.32a12.81,12.81,0,0,0,1.26-.36c0-.22-.12-.49-.22-.81-25.55-85.54-72-143.94-142.17-178.55-56-27.63-118.31-37.12-190.52-29-115.67,13-193.44,73.14-231.15,178.75-35.51,99.4-37.27,204-5.24,310.81,21.25,70.89,62.18,124.05,121.66,158C395.19,833.1,448.28,844,531.51,844ZM659.32,546.37Zm-154.8,82.34a102.18,102.18,0,0,1-44.22-9.79c-21.72-10.24-33.29-26.22-34.38-47.5-1.13-22,8.73-39.46,29.32-52,20.85-12.71,44.41-14.93,64.85-16,3.9-.21,7.78-.15,11.53-.1,1.62,0,3.25,0,4.88,0,17.86-.49,34.72,1.68,54.9,4.57,9.71,1.37,14,6.54,12.78,15.36h0c-3,21.13-6.83,43.47-18.56,63.77-11.47,19.8-28,32.44-49.12,37.58A135,135,0,0,1,504.52,628.71Zm22-113.48c-1.94,0-3.84,0-5.75.13-19,1-40.79,3-59.26,14.29C444.7,539.86,437,553.32,437.89,570.8c.85,16.77,9.85,29,27.52,37.29,20.27,9.54,42.59,11.14,68.25,4.89,18.08-4.39,31.68-14.84,41.58-31.94,10.65-18.43,14.27-39.47,17.07-59.43h0a7,7,0,0,0,.09-1.29,15.35,15.35,0,0,0-2.69-.55c-19.64-2.81-36-4.92-53-4.45h-.17c-1.71,0-3.42,0-5.13-.05S528.11,515.23,526.48,515.23Z" transform="translate(-191.92 -143.98)"/></svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/tiktok.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,4 @@
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Tools -->
+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 256 256" id="Flat" xmlns="http://www.w3.org/2000/svg">
+  <path d="M224,80a52.059,52.059,0,0,1-52-52,4.0002,4.0002,0,0,0-4-4H128a4.0002,4.0002,0,0,0-4,4V156a24,24,0,1,1-34.28418-21.69238,3.99957,3.99957,0,0,0,2.28369-3.61279L92,89.05569a3.99948,3.99948,0,0,0-4.70117-3.938A72.00522,72.00522,0,1,0,172,156l-.00049-42.56348A99.27749,99.27749,0,0,0,224,128a4.0002,4.0002,0,0,0,4-4V84A4.0002,4.0002,0,0,0,224,80Zm-4,39.915a91.24721,91.24721,0,0,1-49.66455-17.1792,4.00019,4.00019,0,0,0-6.33594,3.24707L164,156A64,64,0,1,1,84,94.01223l-.00049,34.271A32.00156,32.00156,0,1,0,132,156V32h32.13184A60.09757,60.09757,0,0,0,220,87.86819Z"/>
+</svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/twitch.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 256 256" id="Flat" xmlns="http://www.w3.org/2000/svg">
+  <path d="M71.99927,244A4.00074,4.00074,0,0,1,68,240V204H48a12.01336,12.01336,0,0,1-12-12V48A12.01336,12.01336,0,0,1,48,36H208a12.01336,12.01336,0,0,1,12,12V156.253a11.96633,11.96633,0,0,1-4.31824,9.21875l-42.89587,35.74707A12.02578,12.02578,0,0,1,165.10364,204H122.89636a4.00351,4.00351,0,0,0-2.56054.92774l-45.77515,38.1455A4.001,4.001,0,0,1,71.99927,244ZM48,44a4.00427,4.00427,0,0,0-4,4V192a4.00426,4.00426,0,0,0,4,4H72a4.00012,4.00012,0,0,1,4,4v31.46l39.21423-32.67871A12.02521,12.02521,0,0,1,122.89636,196h42.20728a4.00356,4.00356,0,0,0,2.56054-.92773l42.89649-35.74707A3.98686,3.98686,0,0,0,212,156.253V48a4.00427,4.00427,0,0,0-4-4Z"/>
+  <path d="M168,140a4.00011,4.00011,0,0,1-4-4V88a4,4,0,0,1,8,0v48A4.00011,4.00011,0,0,1,168,140Z"/>
+  <path d="M120,140a4.00011,4.00011,0,0,1-4-4V88a4,4,0,0,1,8,0v48A4.00011,4.00011,0,0,1,120,140Z"/>
+</svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/twitter.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
+
+<defs>
+</defs>
+<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
+	<path d="M 17.884 19.496 L 38.925 47.63 L 17.751 70.504 h 4.765 l 18.538 -20.027 l 14.978 20.027 h 16.217 L 50.024 40.788 l 19.708 -21.291 h -4.765 L 47.895 37.94 L 34.101 19.496 H 17.884 z M 24.892 23.006 h 7.45 L 65.24 66.993 h -7.45 L 24.892 23.006 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
+	<path d="M 45 90 C 20.187 90 0 69.813 0 45 C 0 20.187 20.187 0 45 0 c 24.813 0 45 20.187 45 45 C 90 69.813 69.813 90 45 90 z M 45 3 C 21.841 3 3 21.841 3 45 c 0 23.159 18.841 42 42 42 c 23.159 0 42 -18.841 42 -42 C 87 21.841 68.159 3 45 3 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
+</g>
+</svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/icons/youtube.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,2 @@
+<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Tools -->
+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><title/><path d="M30.8,53.06a146.19,146.19,0,0,1-15.94-.83A13,13,0,0,1,3.39,40.62a83.16,83.16,0,0,1,.12-17.28A13.07,13.07,0,0,1,15.23,11.91a160.72,160.72,0,0,1,33.6,0A13.07,13.07,0,0,1,60.6,23.64a82.79,82.79,0,0,1-.07,16.95A12.85,12.85,0,0,1,49,52.24h0C42.42,52.79,36.41,53.06,30.8,53.06Zm1.29-40a164.28,164.28,0,0,0-16.65.85A11.06,11.06,0,0,0,5.5,23.56a81.11,81.11,0,0,0-.12,16.87,11,11,0,0,0,9.69,9.82,175.5,175.5,0,0,0,33.79,0h0a10.85,10.85,0,0,0,9.67-9.87,80.8,80.8,0,0,0,.07-16.54,11.09,11.09,0,0,0-10-9.91A153,153,0,0,0,32.09,13Z"/><path d="M26,41.5a1,1,0,0,1-1-1v-17a1,1,0,0,1,1.54-.84l14,9a1,1,0,0,1,0,1.71l-14,8A1,1,0,0,1,26,41.5Zm1-16.17V38.78l11.07-6.33Z"/></svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/ios.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,46 @@
+<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
+  <title>Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917</title>
+  <g>
+    <g>
+      <g>
+        <path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z"/>
+        <path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z" style="fill: #DBD5FF"/>
+      </g>
+      <g id="_Group_" data-name="&lt;Group&gt;">
+        <g id="_Group_2" data-name="&lt;Group&gt;">
+          <g id="_Group_3" data-name="&lt;Group&gt;">
+            <path id="_Path_" data-name="&lt;Path&gt;" d="M24.99671,19.88935a5.14625,5.14625,0,0,1,2.45058-4.31771,5.26776,5.26776,0,0,0-4.15039-2.24376c-1.74624-.1833-3.43913,1.04492-4.329,1.04492-.90707,0-2.27713-1.02672-3.75247-.99637a5.52735,5.52735,0,0,0-4.65137,2.8367c-2.01111,3.482-.511,8.59939,1.41551,11.414.96388,1.37823,2.09037,2.91774,3.56438,2.86315,1.4424-.05983,1.98111-.91977,3.7222-.91977,1.72494,0,2.23035.91977,3.73427.88506,1.54777-.02512,2.52292-1.38435,3.453-2.77563a11.39931,11.39931,0,0,0,1.579-3.21589A4.97284,4.97284,0,0,1,24.99671,19.88935Z"/>
+            <path id="_Path_2" data-name="&lt;Path&gt;" d="M22.15611,11.47681a5.06687,5.06687,0,0,0,1.159-3.62989,5.15524,5.15524,0,0,0-3.33555,1.72582,4.82131,4.82131,0,0,0-1.18934,3.4955A4.26259,4.26259,0,0,0,22.15611,11.47681Z"/>
+          </g>
+        </g>
+        <g>
+          <path d="M42.30178,27.13965h-4.7334l-1.13672,3.35645H34.42678l4.4834-12.418h2.083l4.4834,12.418H43.43752Zm-4.24316-1.54883h3.752L39.961,20.14355H39.9092Z"/>
+          <path d="M55.1592,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238h1.79883v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.64455,21.34766,55.1592,23.16406,55.1592,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30178,29.01563,53.249,27.81934,53.249,25.96973Z"/>
+          <path d="M65.12453,25.96973c0,2.81348-1.50635,4.62109-3.77881,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238h1.79883v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C63.6094,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91064,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26662,29.01563,63.21389,27.81934,63.21389,25.96973Z"/>
+          <path d="M71.70949,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51758-3.61426,2.625,0,4.42383,1.47168,4.48438,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60547,1.626,3.60547,3.44238,0,2.32324-1.84961,3.77832-4.793,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z"/>
+          <path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z"/>
+          <path d="M86.064,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72512,30.6084,86.064,28.82617,86.064,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40137,1.16211-2.40137,3.10742c0,1.96191.89551,3.10645,2.40137,3.10645S92.7593,27.93164,92.7593,25.96973Z"/>
+          <path d="M96.18508,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z"/>
+          <path d="M109.38332,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10109,25.13477Z"/>
+        </g>
+      </g>
+    </g>
+    <g id="_Group_4" data-name="&lt;Group&gt;">
+      <g>
+        <path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z"/>
+        <path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z"/>
+        <path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z"/>
+        <path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z"/>
+        <path d="M59.09377,8.437h.88867v6.26074h-.88867Z"/>
+        <path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z"/>
+        <path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z"/>
+        <path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z"/>
+        <path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z"/>
+        <path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z"/>
+        <path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z"/>
+        <path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z"/>
+        <path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z"/>
+      </g>
+    </g>
+  </g>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/keyboard_backspace.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M17.95 35.9 6 23.95 17.95 12l2.15 2.15-8.3 8.3H42v3H11.8l8.3 8.3Z"/></svg>
\ No newline at end of file
Binary file src/images/logo.png has changed
Binary file src/images/model.jpg has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/nothing.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>
\ No newline at end of file
Binary file src/images/picture.jpg has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/play.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!-- License: CC0. Made by SVG Repo: https://www.svgrepo.com/svg/148207/play-button -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 330 330" style="enable-background:new 0 0 330 330;" xml:space="preserve">
+<path id="XMLID_308_" d="M37.728,328.12c2.266,1.256,4.77,1.88,7.272,1.88c2.763,0,5.522-0.763,7.95-2.28l240-149.999
+	c4.386-2.741,7.05-7.548,7.05-12.72c0-5.172-2.664-9.979-7.05-12.72L52.95,2.28c-4.625-2.891-10.453-3.043-15.222-0.4
+	C32.959,4.524,30,9.547,30,15v300C30,320.453,32.959,325.476,37.728,328.12z"/>
+</svg>
Binary file src/images/shirts.jpg has changed
Binary file src/images/small_logo.png has changed
Binary file src/images/user.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/visibility.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M24 31.5q3.55 0 6.025-2.475Q32.5 26.55 32.5 23q0-3.55-2.475-6.025Q27.55 14.5 24 14.5q-3.55 0-6.025 2.475Q15.5 19.45 15.5 23q0 3.55 2.475 6.025Q20.45 31.5 24 31.5Zm0-2.9q-2.35 0-3.975-1.625T18.4 23q0-2.35 1.625-3.975T24 17.4q2.35 0 3.975 1.625T29.6 23q0 2.35-1.625 3.975T24 28.6Zm0 9.4q-7.3 0-13.2-4.15Q4.9 29.7 2 23q2.9-6.7 8.8-10.85Q16.7 8 24 8q7.3 0 13.2 4.15Q43.1 16.3 46 23q-2.9 6.7-8.8 10.85Q31.3 38 24 38Zm0-15Zm0 12q6.05 0 11.125-3.275T42.85 23q-2.65-5.45-7.725-8.725Q30.05 11 24 11t-11.125 3.275Q7.8 17.55 5.1 23q2.7 5.45 7.775 8.725Q17.95 35 24 35Z"/></svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/images/visibility_off.svg	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="m31.45 27.05-2.2-2.2q1.3-3.55-1.35-5.9-2.65-2.35-5.75-1.2l-2.2-2.2q.85-.55 1.9-.8 1.05-.25 2.15-.25 3.55 0 6.025 2.475Q32.5 19.45 32.5 23q0 1.1-.275 2.175-.275 1.075-.775 1.875Zm6.45 6.45-2-2q2.45-1.8 4.275-4.025Q42 25.25 42.85 23q-2.5-5.55-7.5-8.775Q30.35 11 24.5 11q-2.1 0-4.3.4-2.2.4-3.45.95L14.45 10q1.75-.8 4.475-1.4Q21.65 8 24.25 8q7.15 0 13.075 4.075Q43.25 16.15 46 23q-1.3 3.2-3.35 5.85-2.05 2.65-4.75 4.65Zm2.9 11.3-8.4-8.25q-1.75.7-3.95 1.075T24 38q-7.3 0-13.25-4.075T2 23q1-2.6 2.775-5.075T9.1 13.2L2.8 6.9l2.1-2.15L42.75 42.6ZM11.15 15.3q-1.85 1.35-3.575 3.55Q5.85 21.05 5.1 23q2.55 5.55 7.675 8.775Q17.9 35 24.4 35q1.65 0 3.25-.2t2.4-.6l-3.2-3.2q-.55.25-1.35.375T24 31.5q-3.5 0-6-2.45T15.5 23q0-.75.125-1.5T16 20.15Zm15.25 7.1Zm-5.8 2.9Z"/></svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/index.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,841 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local pairs = Luan.pairs or error()
+local Table = require "luan:Table.luan"
+local is_empty = Table.is_empty or error()
+local concat = Table.concat or error()
+local Html = require "luan:Html.luan"
+local url_encode = Html.url_encode or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local body_header = Shared.body_header or error()
+local footer = Shared.footer or error()
+local User = require "site:/lib/User.luan"
+local current_user = User.current or error()
+
+
+return function()
+	local user = current_user()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+			div[block] > div {
+				max-width: 1000px;
+				margin-left: auto;
+				margin-right: auto;
+			}
+			div[block] p {
+				font-size: 18px;
+			}
+			div[block] h2 {
+				font-size: 28px;
+				color: rgb(76, 61, 174);
+			}
+			div[block] h3 {
+				font-size: 24px;
+				color: rgb(76, 61, 174);
+				margin: 0;
+			}
+
+			div[block="top"] {
+				background-color: rgb(138, 127, 210);
+				text-align: center;
+			}
+			div[block="top"] > div {
+				display: flex;
+				flex-direction: column;
+				align-items: center;
+			}
+			div[block="top"] h1 {
+				color: rgb(255, 235, 15);
+				font-size: 36px;
+				max-width: 12em;
+			}
+			div[block="top"] p {
+				color: rgb(236, 236, 236);
+				max-width: 45em;
+			}
+			div[animation] {
+				width: 98%;
+				aspect-ratio: 950 / 535;
+				position: relative;
+			}
+			div[animation] img {
+				position: absolute;
+				top: 50%;
+				left: 50%;
+				transform: translate(-50%,-50%);
+				animation-duration: 7s;
+				animation-iteration-count: infinite;
+				box-shadow: 5px 5px 20px 1px rgba(0,0,0,0.5);
+				visibility: hidden;
+				animation-play-state: paused;
+			}
+			div[animation] img[circle] {
+				border-radius: 50%;
+			}
+			div[animation] img[bio] {
+				animation-name: bio;
+				width: 12%;
+			}
+			@keyframes bio {
+				0% {
+					opacity: 0;
+					transform: translate(-50%,-50%) scale(1);
+				}
+				15% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(1.4375);
+				}
+				20%, 50% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(1);
+				}
+				60% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(1.1875);
+				}
+				65% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(0.875);
+				}
+				70% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(1.1875);
+				}
+				75% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(0.875);
+				}
+				80%, 100% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(1);
+				}
+			}
+			div[animation] img[i1] {
+				animation-name: i1;
+				width: 14%;
+			}
+			@keyframes i1 {
+				0%, 28% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				38% {
+					opacity: 0.1;
+				}
+				48%, 78% {
+					opacity: 1;
+					top: 61%;
+					left: 89%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			div[animation] img[i2] {
+				animation-name: i2;
+				width: 17%;
+			}
+			@keyframes i2 {
+				0%, 30% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				40% {
+					opacity: 0.1;
+				}
+				50%, 80% {
+					opacity: 1;
+					top: 74%;
+					left: 69%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			div[animation] img[i3] {
+				animation-name: i3;
+				width: 15%;
+			}
+			@keyframes i3 {
+				0%, 26% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				36% {
+					opacity: 0.1;
+				}
+				46%, 76% {
+					opacity: 1;
+					top: 25%;
+					left: 75%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			div[animation] img[i4] {
+				animation-name: i4;
+				width: 13%;
+			}
+			@keyframes i4 {
+				0%, 20% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				30% {
+					opacity: 0.1;
+				}
+				40%, 70% {
+					opacity: 1;
+					top: 76%;
+					left: 32%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			div[animation] img[i5] {
+				animation-name: i5;
+				width: 16%;
+			}
+			@keyframes i5 {
+				0%, 22% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				32% {
+					opacity: 0.1;
+				}
+				42%, 72% {
+					opacity: 1;
+					top: 56%;
+					left: 12%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			div[animation] img[i6] {
+				animation-name: i6;
+				width: 14%;
+			}
+			@keyframes i6 {
+				0%, 24% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				24% {
+					opacity: 0.1;
+				}
+				44%, 74% {
+					opacity: 1;
+					top: 20%;
+					left: 26%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			div[animation] img[ins] {
+				animation-name: ins;
+				width: 7%;
+			}
+			@keyframes ins {
+				0%, 30% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				40% {
+					opacity: 0.1;
+				}
+				50%, 80% {
+					opacity: 1;
+					top: 25%;
+					left: 58%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			div[animation] img[tk] {
+				animation-name: tk;
+				width: 7%;
+			}
+			@keyframes tk {
+				0%, 32% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				42% {
+					opacity: 0.1;
+				}
+				52%, 82% {
+					opacity: 1;
+					top: 45%;
+					left: 35%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			div[animation] img[yt] {
+				animation-name: yt;
+				width: 7%;
+			}
+			@keyframes yt {
+				0%, 34% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				44% {
+					opacity: 0.1;
+				}
+				54%, 84% {
+					opacity: 1;
+					top: 23%;
+					left: 43%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+
+			svg[round] {
+				display: block;
+				margin-top: -1px;
+			}
+
+			div[block="video"] {
+				margin-top: 100px;
+				margin-bottom: 100px;
+			}
+			div[block="video"] > div {
+				display: flex;
+			}
+
+			div[block="midsize"] {
+				background-color: rgb(232, 234, 236);
+				text-align: center;
+				padding-bottom: 60px;
+			}
+			div[block="midsize"] h2 {
+				padding-top: 2em;
+			}
+			div[block="midsize"] p[see] {
+				font-weight: bold;
+			}
+			div[block="midsize"] > div > div {
+				text-align: left;
+				margin-top: 20px;
+			}
+			div[block="midsize"] a {
+				display: block;
+			}
+			div[block="midsize"] img {
+				width: 100%;
+			}
+			div[block="midsize"] > div > div p {
+				font-size: 17px;
+				margin-top: 10px;
+			}
+
+			div[block="users"] {
+				text-align: center;
+			}
+			div[block="users"] h1 {
+				margin-bottom: 0;
+			}
+			div[block="users"] p {
+				margin-bottom: 26px;
+			}
+			div[block="users"] a[more] {
+				text-decoration: none;
+				display: inline-block;
+				background-color: #FFEC0F;
+				padding: 16px 28px;
+				font-size: 20px;
+				border-radius: 4px;
+			}
+			div[block="users"] div[img] {
+				display: flex;
+				margin-top: 20px;
+			}
+			div[block="users"] div[img] span {
+				border: 6px solid white;
+				border-radius: 50%;
+				box-shadow: 5px 5px 20px 1px grey;
+				overflow: hidden;
+			}
+			div[block="users"] div[img] a {
+				display: block;
+				width: 100%;
+				height: 0;
+				padding-top: 100%;
+				position: relative;
+			}
+			div[block="users"] div[img] img {
+				display: block;
+				position: absolute;
+				top: 0;
+				bottom: 0;
+				left: 0;
+				right: 0;
+				width: 100%;
+			}
+
+			div[block="free"] {
+				text-align: center;
+				font-size: 20px;
+			}
+			div[block="free"] > div {
+				display: flex;
+				justify-content: center;
+				align-items: center;
+				gap: 20px;
+			}
+			div[block="free"] div[rect] {
+				background-color: #9181EE;
+				width: 350px;
+				height: 200px;
+				border-radius: 20px;
+				box-shadow: 5px 5px 20px 1px grey;
+			}
+			div[block="free"] div[rect] div[top] {
+				color: #FFEC0F;
+				margin-top: 40px;
+				margin-bottom: 10px;
+			}
+			div[block="free"] div[rect] div[bottom] {
+				display: flex;
+				justify-content: center;
+				align-items: center;
+				gap: 20px;
+			}
+			div[block="free"] div[rect] img {
+				width: 70px;
+				height: 70px;
+			}
+			div[block="free"] div[rect] span {
+				color: white;
+			}
+			div[block="free"] div[plus] {
+				background-color: #FFEC0F;
+				border-radius: 50%;
+				font-size: 25px;
+				width: 40px;
+				height: 40px;
+				display: flex;
+				justify-content: center;
+				align-items: center;
+			}
+
+			div[block="register"] {
+				text-align: center;
+				display: flex;
+				flex-direction: column;
+				align-items: center;
+			}
+			div[block="register"] a[register] {
+				padding: 16px 28px;
+				color: white;
+				background-color: rgb(137, 126, 210);
+				font-size: 20px;
+				border-radius: 4px;
+			}
+			div[block="register"] a[register]:hover {
+				background-color: rgb(164, 151, 252);
+			}
+
+			div[block="feedback"] > div {
+				display: flex;
+				justify-content: center;
+				align-items: center;
+				gap: 20px;
+			}
+			div[block="feedback"] > div > div {
+				max-width: 400px;
+			}
+			div[block="feedback"] img {
+				height: 150px;
+				width: 150px;
+			}
+			div[block="feedback"] h2 {
+				font-size: 28px;
+			}
+
+			@media (min-width: 886px) {
+				div[block] > div {
+					padding-left: 1%;
+					padding-right: 1%;
+				}
+				div[block="video"] > div {
+					justify-content: space-evenly;
+				}
+				div[block="video"] span[text] {
+					max-width: 30%;
+				}
+				div[block="midsize"] > div > div {
+					display: inline-block;
+					width: 45%;
+					margin-left: 2%;
+					margin-right: 2%;
+				}
+				div[block="users"] {
+					margin-top:  60px;
+				}
+				div[block="feedback"],
+				div[block="free"] {
+					margin-top: 80px;
+				}
+				div[block="register"] {
+					margin-top: 40px;
+				}
+				div[block="feedback"],
+				div[block="register"] {
+					padding-bottom:  60px;
+				}
+				div[img] {
+					justify-content: center;
+					gap: 40px;
+				}
+				div[img] span {
+					width: 200px;
+				}
+			}
+
+			@media (max-width: 885px) {
+				div[block] > div {
+					padding-left: 4%;
+					padding-right: 4%;
+				}
+				div[block="video"] > div {
+					flex-direction: column;
+					align-items: center;
+				}
+				div[block="video"] span[text] {
+					max-width: 80%;
+					margin-bottom: 50px;
+				}
+				div[block="free"] {
+					margin-top: 100px;
+					width: 100%;
+				}
+				div[block="free"] > div {
+					flex-direction: column;
+				}
+				div[block="users"] {
+					margin-top: 100px;
+				}
+				div[img] {
+					justify-content: space-evenly;
+				}
+				div[img] span {
+					width: 30%;
+				}
+				div[block="feedback"] {
+					text-align: center;
+				}
+				div[block="feedback"],
+				div[block="register"] {
+					margin-top: 100px;
+					padding-bottom: 100px;
+				}
+				div[block="feedback"] > div {
+					flex-direction: column;
+				}
+			}
+
+			span[video] {
+				cursor: pointer;
+			}
+			div[relative] {
+				position: relative;
+			}
+			span[video] video {
+				display: block;
+				width: 250px;
+			}
+			div[progress] {
+				background-color: #E0E0E0;
+			}
+			div[progress] div {
+				height: 6px;
+				background-color: #52A08E;
+				width: 0;
+			}
+			@media (hover: hover) {
+				div[progress]:hover {
+					background-color: #CECECE;
+				}
+				div[progress]:hover div {
+					background-color: #31665A;
+				}
+			}
+			div[play] {
+				position: absolute;
+				top: 0;
+				left: 0;
+				bottom: 0;
+				right: 0;
+				display: none;
+				justify-content: center;
+				align-items: center;
+			}
+			div[play] img {
+				width: 60px;
+				filter: invert(100%);
+				opacity: 0.7;
+				user-select: none;
+			}
+		</style>
+		<script>
+			function mouseAction(event,progress) {
+				//console.log(event);
+				if( event.buttons === 0 )
+					return;
+				let video = progress.parentNode.querySelector('video');
+				video.currentTime = video.duration * event.offsetX / progress.clientWidth;
+			}
+
+			function touchMove(event,progress) {
+				//console.log(event);
+				let touches = event.touches;
+				if( touches.length !== 1 )
+					return;
+				let touch = touches[0];
+				let touchX = touch.clientX;
+				let rect = event.target.getBoundingClientRect();
+				let targetX = rect.x;
+				if( touchX < targetX || touchX > rect.right )
+					return;
+				event.offsetX = touchX - targetX;
+				//console.log(event.offsetX);
+				mouseAction(event,progress);
+			}
+
+			function stop(video) {
+				video.pause();
+				let play = video.nextElementSibling;
+				play.style.display = 'flex';
+			}
+
+			function play(video) {
+				video.play();
+				let play = video.nextElementSibling;
+				play.style.display = 'none';
+			}
+
+			function updateProgress(video) {
+				//console.log('init');
+				let progress = video.parentNode.parentNode.querySelector('div[progress] div');
+				let pct = 100*video.currentTime/video.duration;
+				//console.log(pct);
+				progress.style.width = pct+'%';
+			}
+
+			function resized() {
+				let imgs = document.querySelectorAll('div[animation] img[rect]');
+				for( let img of imgs ) {
+					img.style['border-radius'] = 0.3 * img.width + 'px';
+				}
+			}
+			function loaded() {
+				resized();
+				let imgs = document.querySelectorAll('div[animation] img');
+				for( let img of imgs ) {
+					img.style.visibility = 'visible';
+					img.style['animation-play-state'] = 'running';
+				}
+			}
+		</script>
+	</head>
+	<body onload="loaded()" onresize="resized()">
+	<div full>
+<%		body_header() %>
+		<div body>
+			<div block=top>
+				<div>
+					<h1>The link in bio that increases affiliate sales.</h1>
+					<div animation>
+						<img bio rect src="/images/home/bio.png">
+						<img i1 rect src="/images/home/i1.png">
+						<img i2 rect src="/images/home/i2.png">
+						<img i3 rect src="/images/home/i3.png">
+						<img i4 rect src="/images/home/i4.png">
+						<img i5 rect src="/images/home/i5.png">
+						<img i6 rect src="/images/home/i6.png">
+						<img ins circle src="/images/home/ins.png">
+						<img tk circle src="/images/home/tk.png">
+						<img yt circle src="/images/home/yt.png">
+					</div>
+					<p>One link to all of your social media posts with affiliated products.</p>
+					<p>People that switch to LinkMyStyle report that their sales increased by over 100%. LinkMyStyle is completely free to use, so there is no risk for you to try it out and see how well it works for you.</p>
+				</div>
+			</div>
+			<svg round viewBox="0 0 100 7" xmlns="http://www.w3.org/2000/svg">
+				<circle cx="50" cy="-193" r="200" fill="rgb(138, 127, 210)" />
+			</svg>
+			<div block=video>
+				<div>
+					<span text>
+						<h2>A fast lane to showcase products</h2>
+						<p>Your followers can now easily find the products shown in your videos.</p>
+					</span>
+					<span video>
+						<div relative>
+							<video src="https://ucarecdn.com/9be9b1b8-98a9-4f12-8cd4-992de8d3dc70/" muted autoplay loop playsinline onclick="stop(this)" ontimeupdate="updateProgress(this)">
+							</video>
+							<div play onclick="play(previousElementSibling)"><img src="/images/play.svg"></div>
+						</div>
+						<div progress onmousedown="mouseAction(event,this)" onmousemove="mouseAction(event,this)" ontouchstart="touchMove(event,this)" ontouchmove="touchMove(event,this)">
+							<div></div>
+						</div>
+					</span>
+				</div>
+			</div>
+			<div block=midsize>
+				<div>
+					<h2>User Example</h2>
+					<p see>See how @JessicaGrace increased her affiliated sales by over 100%.</p>
+					<div>
+						<a href="https://www.tiktok.com/@jessica_gracexx"><img src="/images/home/01.png"></a>
+						<h3>1. Add a link to bio.</h3>
+						<p>She puts her LinkMyStyle link in both her TikTok and Instagram bios so users can easily find the products shown in her videos.</p>
+					</div>
+					<div>
+						<a href="https://www.tiktok.com/@jessica_gracexx/video/7321390538190540064"><img src="/images/home/02.png"></a>
+						<h3>2. Post a photo with links.</h3>
+						<p>She posts videos where she shows her outfits to her followers, and then posts a screenshot to LinkMyStyle with the product links.</p>
+					</div>
+					<div>
+						<a href="https://linkmy.style/JessicaGrace#pc9209"><img src="/images/home/03.png"></a>
+						<h3>3. Followers find the product.</h3>
+						<p>Users then go to her LinkMyStyle page and look for the thumbnail to the video they just saw.</p>
+					</div>
+					<div>
+						<a href="https://linkmy.style/pic.html?pic=9209"><img src="/images/home/04.png"></a>
+						<h3>4. Followers click and go.</h3>
+						<p>After clicking on the thumbnail, they can easily find the products shown in her video.</p>
+					</div>
+				</div>
+			</div>
+			<div block=users>
+				<div>
+					<h2>User Highlights</h2>
+					<p><a more href="https://linkmy.style/linkmystyle">View More</a></p>
+					<div img>
+						<span><a href="https://linkmy.style/hyn_x"><img src="/images/home/hyn_x.jpeg"></a></span>
+						<span><a href="https://linkmy.style/JessicaGrace"><img src="/images/home/midsize.jpeg"></a></span>
+						<span><a href="https://linkmy.style/kenziekayfashion"><img src="/images/home/kenzie.jpeg"></a></span>
+					</div>
+				</div>
+			</div>
+			<div block=free>
+				<div>
+					<div rect>
+						<div top>Free Custom Backgrounds</div>
+						<div bottom>
+							<img src="/images/home/background.svg">
+							<span>Make your page<br>truly yours</span>
+						</div>
+					</div>
+					<div plus><span>+</span></div>
+					<div rect>
+						<div top>Free Analytics</div>
+						<div bottom>
+							<img src="/images/home/analytics.svg">
+							<span>See what your<br>audience is doing</span>
+						</div>
+					</div>
+				</div>
+			</div>
+<%	if user ~= nil then %>
+			<div block=feedback>
+				<div>
+					<div>
+						<h2>We would love to get your feedback about our website</h2>
+						<p black>Email us your thoughts, suggestions, or support questions.<br>support@linkmy.style</p>
+					</div>
+					<img src="/images/home/mail.svg">
+				</div>
+			</div>
+<%	else %>
+			<div block=register>
+				<div>
+					<h2>Completely Free to Use</h2>
+					<div>
+						<a button register href="/register.html">Register Now</a>
+					</div>
+				</div>
+			</div>
+<%	end %>
+		</div>
+<%		footer() %>
+	</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/init.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,69 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local String = require "luan:String.luan"
+local substring = String.sub or error()
+local regex = String.regex or error()
+local contains = String.contains or error()
+local Table = require "luan:Table.luan"
+local concat = Table.concat or error()
+local Time = require "luan:Time.luan"
+local Thread = require "luan:Thread.luan"
+local Http = require "luan:http/Http.luan"
+local Hosted = require "luan:host/Hosted.luan"
+local User = require "site:/lib/User.luan"
+local get_user_by_name = User.get_by_name or error()
+local name_regex = User.name_regex
+local main_html = require "site:/lib/main_html.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "init"
+
+
+Hosted.set_https and Hosted.set_https(true)
+
+local bad_bots = {
+	[[facebookexternalhit]]
+	[[VivoBrowser]]
+	[[\QFirefox/3.\E]]
+	[[\QFirefox/50.\E]]
+	[[SemrushBot]]
+	[[LightspeedSystemsCrawler]]
+	[[bingbot]]
+	[[PetalBot]]
+	[[\QChrome/83.\E]]
+}
+local bad_bots_ptn = regex(concat(bad_bots,"|"))
+
+function Http.error_priority(e)
+	local request = Http.request or error()
+	local agent = request.headers["user-agent"]
+	local referrer = request.headers.referer
+	if agent~=nil and bad_bots_ptn.matches(agent) then return "info" end
+	if e.priority ~= nil then return e.priority end
+	if referrer==nil or contains(referrer,"baidu.com") or contains(referrer,"google.com") then return "warn" end
+	return "error"
+end
+
+function Http.not_found_handler()
+	local s = substring(Http.request.path,2)
+	--logger.info(s)
+	if not name_regex.matches(s) then
+		return false
+	end
+	local user = get_user_by_name(s)
+	if user == nil then
+		return false
+	end
+	main_html(user)
+	return true
+end
+
+logger.error "init"
+local function log_day()
+	logger.error("day")
+end
+Thread.schedule( log_day, { repeating_delay=Time.period{days=1} } )
+
+local Uploadcare = require "site:/lib/Uploadcare.luan"
+Thread.schedule( Uploadcare.gc, { repeating_delay=Time.period{days=1} } )
+
+return true
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/instagram.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,26 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<script>
+			setTimeout(function(){
+				location = 'https://www.instagram.com/linkmy.style/';
+			},1000);
+		</script>
+	</head>
+	<body>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/job.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,64 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Utils = require "site:/lib/Utils.luan"
+local base_url = Utils.base_url or error()
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local body_header = Shared.body_header or error()
+local footer = Shared.footer or error()
+local User = require "site:/lib/User.luan"
+
+
+return function()
+	local user = User.current_required()
+	if user==nil then return end
+	local url = base_url().."/"..user.name
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+			div[body] {
+				max-width: 700px;
+				margin-left: auto;
+				margin-right: auto;
+				padding-left: 20px;
+				padding-right: 20px;
+			}
+		</style>
+	</head>
+	<body>
+	<div full>
+<%		body_header() %>
+		<div body>
+			<h1>Create Facebook/Instagram Ad</h1>
+
+			<p>The job is to create a Facebook ad that will generate clicks to our site and registrations.  We will give you access to our Facebook ad account to see how your ad performs compared to our other ads.  You may update your ad if you think of improvements after your first version.  If your ad performs well, we will probably hire you again for future ads.</p>
+
+			<p>Please look at <a href="/">our homepage</a> to see what we do.  Most of our traffic comes from Instagram.</p>
+
+			<p>We believe that ads with content familiar to the viewer will be most effective.  So we have created Facebook audiences based on which of our user's webpages people have visited.  We are starting with <a href="https://linkmy.style/JessicaGrace">JessicaGrace</a>.  So we are using an audience of people who have visited JessicaGrace's LinkMyStyle page.  This audience will be followers of JessicaGrace who have seen LinkMyStyle.  So we hope that ads containing JessicaGrace will catch their attention, and maybe they will also remember LinkMyStyle.  Your ad will be in a Facebook AdSet that only targets this audience.</p>
+
+			<p>But most of this audience is actually useless for us.  We specifically want the small percentage of this audience who are influencers doing affiliate marketing.  This is why we want to emphasize the benefits for affiliates in our ads.</p>
+
+			<p>We emailed JessicaGrace asking if she is satisfied with our service and she responded "Loving the platform so much - Ive seen a huge increase in sales - over 100% increase actually.".  You can use any of this in your ad.  You can also use photos from her Instagram account.</p>
+
+			<p>The design of your ad is up to you.  You can use any format - image, carousel, or video.  You can create your ad in Facebook's "Creative Hub" so that we can use it directly.  If you work with us, just send us the email you use for Facebook and we will add you to our ad account.</p>
+
+			<p>To track your ad performance, use the Facebook report "fs ad2".  We use "Content Views" as a general metric to track everything we care about including visits, registrations, etc.  The best ads are the ones with the lowest "Cost per content view".</p>
+
+			<p>If you have any questions, please contact gina@linkmy.style .</p>
+
+			<p>&nbsp;</p>
+		</div>
+<%		footer() %>
+	</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Ab_test.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,102 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local pairs = Luan.pairs or error()
+local String = require "luan:String.luan"
+local starts_with = String.starts_with or error()
+local Math = require "luan:Math.luan"
+local random = Math.random or error()
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local uri = Io.uri or error()
+local output_to = Io.output_to or error()
+local Http = require "luan:http/Http.luan"
+local Utils = require "site:/lib/Utils.luan"
+local list_to_set = Utils.list_to_set or error()
+local compressed = Utils.compressed or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "Ab_test"
+
+
+local Ab_test = {}
+
+Ab_test.tests = {
+--[[
+	dummy = {
+		values = { "a", "b" }
+	}
+--]]
+	home = {
+		values = { "old", "new" }
+	}
+}
+
+local function write(fn)
+	local request = Http.request
+	if request.ab_uri == nil then
+		request.ab_uri = uri("string:")
+		request.ab_out = request.ab_uri.text_writer()
+	end
+	output_to( request.ab_out, fn )
+end
+
+local cookie_names = list_to_set{}
+
+do
+	local function set_cookie(name,value)
+		Http.response.set_persistent_cookie( name, value )
+		Http.request.cookies[name] = value
+	end
+
+	for name, info in pairs(Ab_test.tests) do
+		local cookie_name = "test_"..name
+		local values = info.values
+		cookie_names[cookie_name] = true
+
+		local value_set = list_to_set(values)
+	
+		function info.value()
+			local value = Http.request.cookies[cookie_name]
+			if not value_set[value] then
+				value = values[random(#values)]
+				set_cookie( cookie_name, value )
+				write( function() %>
+				setAbTest( <%=json_string(cookie_name)%>, <%=json_string(values,compressed)%> );
+<%				end )
+			end
+			return value
+		end
+	
+		function info.has_value()
+			return Http.request.cookies[cookie_name] ~= nil
+		end
+
+	end_for
+end_do
+
+local function remove_tests()
+	write( function() %>
+			removeAbTest('test_dummy');
+<%	end )
+end
+
+function Ab_test.head()
+	for cookie_name in pairs(Http.request.cookies) do
+		if starts_with(cookie_name,"test_") and not cookie_names[cookie_name] then
+			Http.response.remove_cookie(cookie_name)
+		end
+	end
+	-- remove_tests()
+	local request = Http.request
+	if request.ab_uri ~= nil then
+		request.ab_out.close()
+		request.ab_out = nil
+%>
+		<script>
+<%=			request.ab_uri.read_text() %>
+		</script>
+<%
+	end
+end
+
+return Ab_test
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Db.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,60 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local new_error = Luan.new_error or error()
+local ipairs = Luan.ipairs or error()
+local Lucene = require "luan:lucene/Lucene.luan"
+local Io = require "luan:Io.luan"
+local uri = Io.uri or error()
+local Http = require "luan:http/Http.luan"
+local Thread = require "luan:Thread.luan"
+local Time = require "luan:Time.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "Db"
+
+
+local dir = uri("site:/private/local/lucene")
+
+local Db = Lucene.index( dir, {
+	log_dir = uri("site:/private/local/lucene_log")
+	name = "lucene"
+	version = 1
+} )
+
+Db.indexed_fields.user_email = Lucene.type.lowercase
+Db.indexed_fields.user_name = Lucene.type.lowercase
+Db.indexed_fields.user_registered = Lucene.type.long
+
+Db.indexed_fields.link_owner_id = Lucene.type.long
+Db.indexed_fields.link_user_id = Lucene.type.long
+Db.indexed_fields.link_order = Lucene.type.float
+
+Db.indexed_fields.pic_user_id = Lucene.type.long
+Db.indexed_fields.pic_order = Lucene.type.float
+
+Db.indexed_fields.icon_name = Lucene.type.string
+Db.indexed_fields.icon_user_id = Lucene.type.long
+Db.indexed_fields.icon_order = Lucene.type.float
+
+function Db.not_in_transaction()
+	logger.error(new_error("not in transaction"))
+end
+
+Db.restore_from_log()
+
+Db.update{
+	[1] = function()
+		local docs = Db.search("type:pic",1,1000000)
+		for _, doc in ipairs(docs) do
+			if doc.title == nil then
+				doc.title = "Error"
+				Db.save(doc)
+			end
+		end
+	end
+}
+
+if Http.is_serving then
+	Thread.schedule( Db.check, { delay=0, repeating_delay=Time.period{hours=1} } )
+end
+
+return Db
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Facebook.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,101 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local to_string = Luan.to_string or error()
+local stringify = Luan.stringify or error()
+local String = require "luan:String.luan"
+local trim = String.trim or error()
+local to_lower = String.lower or error()
+local digest_message = String.digest_message or error()
+local Time = require "luan:Time.luan"
+local time_now = Time.now or error()
+local Number = require "luan:Number.luan"
+local long = Number.long or error()
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local json_parse = Parsers.json_parse or error()
+local Io = require "luan:Io.luan"
+local uri = Io.uri or error()
+local Config = require "site:/private/Config.luan"
+local access_token = Config.facebook.access_token or error()
+local Shared = require "site:/lib/Shared.luan"
+local is_test = not Shared.is_production
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "Facebook"
+
+
+local pixel_id = "667025338202310"
+local url = "https://graph.facebook.com/v17.0/"..pixel_id.."/events"
+
+
+local Facebook = {}
+
+local function call(events)
+	local user_data = events[1].user_data  -- assume they are all the same
+	local em = user_data.em
+	if em ~= nil then
+		em = to_lower(trim(em))
+		user_data.em = digest_message("SHA-256",em)
+	end
+	local external_id = user_data.external_id
+	if external_id ~= nil then
+		external_id = to_string(external_id)
+		if is_test then
+			external_id = "test_"..external_id
+		end
+		user_data.external_id = digest_message("SHA-256",external_id)
+	end
+	if is_test then
+		for _, event in ipairs(events) do
+			event.event_name = "test_"..event.event_name
+		end
+	end
+	logger.info(json_string(events))
+	local parameters = {
+		access_token = access_token
+		data = json_string(events)
+	}
+	if is_test then
+		parameters.test_event_code = "TEST"
+	end
+	local options = {
+		method = "POST"
+		parameters = parameters
+	}
+	return uri(url,options).read_text()
+end
+Facebook.call = call
+
+function Facebook.track_visit(user)
+	local user_data = {
+		em = user.email or error()
+		external_id = user.id or error()
+	}
+	local event_time = long( time_now() // 1000 )
+	local events = {
+		{
+			event_name = "Contact"
+			event_time = event_time
+			user_data = user_data
+			action_source = "other"
+		}
+		{
+			event_name = "Purchase"
+			event_time = event_time
+			user_data = user_data
+			action_source = "other"
+			custom_data = {
+				value = 1
+				currency = "USD"
+			}
+		}
+	}
+	try
+		local result = call(events)
+		logger.info(result)
+	catch e
+		logger.error(e.."\n"..e.response_content.."\nevents = "..stringify(events))
+	end
+end
+
+return Facebook
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Icon.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,167 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local Number = require "luan:Number.luan"
+local long = Number.long or error()
+local float = Number.float or error()
+local Db = require "site:/lib/Db.luan"
+
+
+local Icon = {}
+
+local icon_names = {
+	applemusic = {
+		title = "Apple Music"
+		placeholder = "https://music.apple.com/"
+	}
+	cashapp = {
+		title = "Cashapp"
+		placeholder = "https://cash.app/$username"
+	}
+	discord = {
+		title = "Discord"
+		placeholder = "https://discord.com/invite/yourchannel"
+	}
+	email = {
+		title = "Email"
+		placeholder = "you@email.com"
+		type = "email"
+	}
+	facebook = {
+		title = "Facebook"
+		placeholder = "https://facebook.com/facebookpageurl"
+	}
+	instagram = {
+		title = "Instagram"
+		placeholder = "https://www.instagram.com/username"
+	}
+	patreon = {
+		title = "Patreon"
+		placeholder = "https://patreon.com/"
+	}
+	pinterest = {
+		title = "Pinterest"
+		placeholder = "https://www.pinterest.com/username/"
+	}
+	paypal = {
+		title = "PayPal"
+		placeholder = "https://www.paypal.me/username"
+	}
+	reddit = {
+		title = "Reddit"
+		placeholder = "https://www.reddit.com/"
+	}
+	snapchat = {
+		title = "Snapchat"
+		placeholder = "https://www.snapchat.com/add/username"
+	}
+	soundcloud = {
+		title = "SoundCloud"
+		placeholder = "https://soundcloud.com/username"
+	}
+	spotify = {
+		title = "Spotify"
+		placeholder = "https://open.spotify.com/artist/artistname"
+	}
+	threads = {
+		title = "Threads"
+		placeholder = "https://www.threads.net/@username"
+	}
+	tiktok = {
+		title = "TikTok"
+		placeholder = "https://www.tiktok.com/@username"
+	}
+	twitch = {
+		title = "Twitch"
+		placeholder = "https://twitch.tv/"
+	}
+	twitter = {
+		title = "Twitter"
+		placeholder = "https://twitter.com/username"
+	}
+	youtube = {
+		title = "YouTube"
+		placeholder = "https://youtube.com/channel/youtubechannelurl"
+	}
+}
+Icon.icon_names = icon_names
+
+local function from_doc(doc)
+	doc.type == "icon" or error "wrong type"
+	return Icon.new {
+		id = doc.id
+		name = doc.icon_name
+		url = doc.url
+		user_id = doc.icon_user_id
+		order = doc.icon_order
+	}
+end
+Icon.from_doc = from_doc
+
+local function to_doc(icon)
+	return {
+		type = "icon"
+		id = icon.id
+		icon_name = icon.name or error()
+		url = icon.url or error()
+		icon_user_id = long(icon.user_id)
+		icon_order = float(icon.order)
+	}
+end
+
+function Icon.new(icon)
+
+	function icon.save()
+		local doc = to_doc(icon)
+		Db.save(doc)
+		icon.id = doc.id
+	end
+
+	function icon.title_attr()
+		return [[title="]]..icon_names[icon.name].title..[["]]
+	end
+
+	function icon.move_after(prev)
+		local icons = Icon.get_user_icons(icon.user_id)
+		if prev == nil then
+			icon.order = icons[1].order - 1
+		else
+			icon.owner_id==prev.owner_id or error()
+			for i, x in ipairs(icons) do
+				if prev.id == x.id then
+					if i == #icons then
+						icon.order = prev.order + 1
+					else
+						local next = icons[i+1]
+						icon.order = (prev.order + next.order) / 2
+					end
+					return
+				end
+			end
+			error()
+		end
+	end
+
+	return icon
+end
+
+function Icon.get_by_id(id)
+	local doc = Db.get_document("id:"..id)
+	return doc and from_doc(doc)
+end
+
+local function from_docs(docs)
+	local icons = {}
+	for _, doc in ipairs(docs) do
+		local icon = from_doc(doc)
+		icons[#icons+1] = icon
+	end
+	return icons
+end
+
+function Icon.get_user_icons(user_id)
+	local docs = Db.search("icon_user_id:"..user_id,1,1000,{sort="icon_order"})
+	return from_docs(docs)
+end
+
+return Icon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Link.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,100 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local stringify = Luan.stringify or error()
+local Number = require "luan:Number.luan"
+local long = Number.long or error()
+local float = Number.float or error()
+local Db = require "site:/lib/Db.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "Link"
+
+
+local Link = {}
+
+local function from_doc(doc)
+	doc.type == "link" or error "wrong type"
+	return Link.new {
+		id = doc.id
+		url = doc.url
+		title = doc.title
+		owner_id = doc.link_owner_id
+		user_id = doc.link_user_id
+		order = doc.link_order
+	}
+end
+
+local function to_doc(link)
+	return {
+		type = "link"
+		id = link.id
+		url = link.url or error()
+		title = link.title or error()
+		link_owner_id = long(link.owner_id)
+		link_user_id = long(link.user_id)
+		link_order = float(link.order)
+	}
+end
+
+function Link.new(link)
+
+	function link.save()
+		local doc = to_doc(link)
+		Db.save(doc)
+		link.id = doc.id
+	end
+
+	function link.reload()
+		return Link.get_by_id(link.id) or error(link.id)
+	end
+
+	function link.delete()
+		Db.run_in_transaction( function()
+			Db.delete("id:"..link.id)
+		end )
+	end
+
+	function link.move_after(prev)
+		local links = Link.get_owner_links(link.owner_id)
+		if prev == nil then
+			link.order = links[1].order - 1
+		else
+			link.owner_id==prev.owner_id or error()
+			for i, x in ipairs(links) do
+				if prev.id == x.id then
+					if i == #links then
+						link.order = prev.order + 1
+					else
+						local next = links[i+1]
+						link.order = (prev.order + next.order) / 2
+					end
+					return
+				end
+			end
+			error()
+		end
+	end
+
+	return link
+end
+
+function Link.get_by_id(id)
+	local doc = Db.get_document("id:"..id)
+	return doc and from_doc(doc)
+end
+
+local function from_docs(docs)
+	local links = {}
+	for _, doc in ipairs(docs) do
+		local link = from_doc(doc)
+		links[#links+1] = link
+	end
+	return links
+end
+
+function Link.get_owner_links(owner_id)
+	local docs = Db.search("link_owner_id:"..owner_id,1,1000,{sort="link_order"})
+	return from_docs(docs)
+end
+
+return Link
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Pic.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,144 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local Number = require "luan:Number.luan"
+local long = Number.long or error()
+local float = Number.float or error()
+local Table = require "luan:Table.luan"
+local concat = Table.concat or error()
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Db = require "site:/lib/Db.luan"
+local Uploadcare = require "site:/lib/Uploadcare.luan"
+local uploadcare_url = Uploadcare.url or error()
+local uploadcare_thumb_url = Uploadcare.thumb_url or error()
+local Utils = require "site:/lib/Utils.luan"
+local to_list = Utils.to_list or error()
+
+
+local Pic = {}
+
+local function from_doc(doc)
+	doc.type == "pic" or error "wrong type"
+	return Pic.new {
+		id = doc.id
+		uuid = doc.uuid
+		filename = doc.filename
+		user_id = doc.pic_user_id
+		order = doc.pic_order
+		title = doc.title
+		is_hidden = doc.is_hidden=="true"
+		hashtags = doc.hashtags
+	}
+end
+
+local function to_doc(pic)
+	return {
+		type = "pic"
+		id = pic.id
+		uuid = pic.uuid or error()
+		filename = pic.filename or error()
+		pic_user_id = long(pic.user_id)
+		pic_order = float(pic.order)
+		title = pic.title or error()
+		is_hidden = pic.is_hidden and "true" or nil
+		hashtags = pic.hashtags
+	}
+end
+
+function Pic.new(pic)
+
+	function pic.save()
+		local doc = to_doc(pic)
+		Db.save(doc)
+		pic.id = doc.id
+	end
+
+	function pic.reload()
+		return Pic.get_by_id(pic.id) or error(pic.id)
+	end
+
+	function pic.delete()
+		Db.run_in_transaction( function()
+			local id = pic.id
+			Db.delete("link_owner_id:"..id)
+			Db.delete("id:"..id)
+		end )
+	end
+
+	function pic.get_url()
+		return uploadcare_url(pic.uuid)
+	end
+
+	function pic.get_thumb_url()
+		return uploadcare_thumb_url(pic.uuid)
+	end
+
+	function pic.title_attr()
+		local title = pic.title
+		return [[title="]]..html_encode(title)..[["]]
+	end
+
+	function pic.move_after(prev)
+		local pics = Pic.get_user_pics(pic.user_id)
+		if prev == nil then
+			pic.order = pics[1].order - 1
+		else
+			pic.user_id==prev.user_id or error()
+			for i, x in ipairs(pics) do
+				if prev.id == x.id then
+					if i == #pics then
+						pic.order = prev.order + 1
+					else
+						local next = pics[i+1]
+						pic.order = (prev.order + next.order) / 2
+					end
+					return
+				end
+			end
+			error()
+		end
+	end
+
+	function pic.hashtags_html(base_path)
+		local hashtags = pic.hashtags
+		if hashtags == nil then
+			return ""
+		end
+		hashtags = to_list(hashtags)
+		local t = {}
+		for _, hashtag in ipairs(hashtags) do
+			t[#t+1] = `%><a href="<%=base_path%>#<%=hashtag%>">#<%=hashtag%></a><%`
+		end
+		return concat(t," ")
+	end
+
+	return pic
+end
+
+function Pic.get_by_id(id)
+	local doc = Db.get_document("id:"..id)
+	return doc and from_doc(doc)
+end
+
+local function from_docs(docs)
+	local pics = {}
+	for _, doc in ipairs(docs) do
+		local pic = from_doc(doc)
+		pics[#pics+1] = pic
+	end
+	return pics
+end
+
+function Pic.get_user_pics(user_id)
+	local docs = Db.search("pic_user_id:"..user_id,1,1000,{sort="pic_order"})
+	return from_docs(docs)
+end
+
+function Pic.search(query,sort,rows)
+	rows = rows or 1000000
+	local docs = Db.search(query,1,rows,{sort=sort})
+	return from_docs(docs)
+end
+
+return Pic
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Reporting.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,166 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local stringify = Luan.stringify or error()
+local pairs = Luan.pairs or error()
+local ipairs = Luan.ipairs or error()
+local range = Luan.range or error()
+local Io = require "luan:Io.luan"
+local uri = Io.uri or error()
+local Number = require "luan:Number.luan"
+local long = Number.long or error()
+local Time = require "luan:Time.luan"
+local time_now = Time.now or error()
+local Table = require "luan:Table.luan"
+local copy = Table.copy or error()
+local sort = Table.sort or error()
+local Thread = require "luan:Thread.luan"
+local Lucene = require "luan:lucene/Lucene.luan"
+local Shared = require "site:/lib/Shared.luan"
+local compressed = Shared.compressed or error()
+local Utils = require "site:/lib/Utils.luan"
+local list_to_set = Utils.list_to_set or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "Reporting"
+
+
+local Reporting = {}
+
+local dir = uri("site:/private/local/reporting")
+
+local db = Lucene.index( dir )
+Reporting.db = db
+
+db.indexed_fields.owner = Lucene.type.string
+db.indexed_fields.value = Lucene.type.string
+db.indexed_fields.day = Lucene.type.long
+
+
+local day = Time.period{days=1}
+local days = Time.period{days=30}
+local offset_from_GMT = Time.period{hours=7}
+
+local function today()
+	local today = (time_now() - offset_from_GMT) // day * day + offset_from_GMT
+	return long(today)
+end
+
+local function by_count(rec1,rec2)
+	return rec1.count > rec2.count
+end
+
+local limit = 20
+
+function Reporting.get_data( query, sum_by )
+	local first_day = long(today() - days)
+	query = query.." +day:["..first_day.." TO *]"
+	local data
+	if sum_by == nil then
+		data = db.search( query, 1, 1000000 )
+	else
+		local t = {}
+		db.advanced_search( query, function(_,doc_fn,_)
+			local doc = doc_fn()
+			local key = doc[sum_by]
+			if key == nil then
+				logger.warn("missing '"..sum_by.."' in "..stringify(doc))
+				return
+			end
+			local count = doc.count or error()
+			local found = t[key]
+			if found == nil then
+				t[key] = {
+					[sum_by] = key
+					count = count
+				}
+			else
+				found.count = found.count + count
+			end
+		end )
+		data = {}
+		for _, val in pairs(t) do
+			data[#data+1] = val
+		end
+	end
+
+	-- for graph
+	if sum_by == nil or sum_by == "day" then
+		local last_day = today()
+		local first_day = last_day - days
+		local map = {}
+		for _, el in ipairs(data) do
+			map[long(el.day)] = el.count
+		end
+		data = {}
+		for date in range(first_day,last_day,day) do
+			date = long(date)
+			local count = map[date] or 0
+			data[#data+1] = {date,count}
+		end
+		return data
+	else
+		sort(data,by_count)
+		local t = {nil}
+		local n = 0
+		for _, el in ipairs(data) do
+			t[#t+1] = { x=el[sum_by], y=el.count }
+			n = n + 1
+			if n == limit then
+				break
+			end
+		end
+		return t
+	end
+end
+
+local function delete_old()
+	logger.warn "delete_old"
+	local expired = long( today() - Time.period{days=31} )
+	db.delete( "day:[* TO "..expired.."}" )
+end
+-- delete_old()
+Thread.schedule( delete_old, { repeating_delay=Time.period{days=1} } )
+
+
+-- for ads
+
+db.indexed_fields.ad_visit_owner_id = Lucene.type.long
+
+local visit_period = Time.period{hours=1}
+-- visit_period = Time.period{seconds=1}
+
+function Reporting.maybe_track_visit(owner)
+	local doc = db.get_document{ ad_visit_owner_id = owner.id }
+	return doc == nil or doc.time + visit_period < time_now()
+end
+
+function Reporting.should_track_visit(owner)
+	return db.run_in_transaction( function()
+		local now = time_now()
+		local doc = db.get_document{ ad_visit_owner_id = owner.id }
+		if doc == nil then
+			db.save{
+				type = "ad_visit"
+				ad_visit_owner_id = owner.id
+				time = now
+			}
+			return true
+		elseif doc.time + visit_period < now then
+			doc.time = now
+			db.save(doc)
+			return true
+		else
+			return false
+		end
+	end )
+end
+
+function Reporting.owners_with_visits()
+	local owner_ids = list_to_set{}
+	local docs = db.search("type:ad_visit",1,1000000)
+	for _, doc in ipairs(docs) do
+		owner_ids[doc.ad_visit_owner_id] = true
+	end
+	return owner_ids
+end
+
+return Reporting
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Shared.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,358 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local pairs = Luan.pairs or error()
+local range = Luan.range or error()
+local Table = require "luan:Table.luan"
+local is_list = Table.is_list or error()
+local is_empty = Table.is_empty or error()
+local concat = Table.concat or error()
+local Io = require "luan:Io.luan"
+local uri = Io.uri or error()
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local url_encode = Html.url_encode or error()
+local Parsers = require "luan:Parsers.luan"
+local json_parse = Parsers.json_parse or error()
+local json_string = Parsers.json_string or error()
+local Thread = require "luan:Thread.luan"
+local thread_run = Thread.run or error()
+local sleep = Thread.sleep or error()
+local Time = require "luan:Time.luan"
+local Http = require "luan:http/Http.luan"
+local Mail = require "luan:mail/Mail.luan"
+local Config = require "site:/private/Config.luan"
+local uploadcare_public_key = Config.uploadcare.public_key or error()
+local Ab_test = require "site:/lib/Ab_test.luan"
+local ab_test_head = Ab_test.head or error()
+local User = require "site:/lib/User.luan"
+local current_user = User.current or error()
+local Icon = require "site:/lib/Icon.luan"
+local get_user_icons = Icon.get_user_icons or error()
+local Utils = require "site:/lib/Utils.luan"
+local is_production = not not Utils.is_production
+local base_url = Utils.base_url or error()
+local to_list = Utils.to_list or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "Shared"
+
+
+local Shared = {}
+
+Shared.has_facebook = false;
+
+local started = Time.now()
+
+Shared.is_production = is_production
+local is_local = Http.domain == nil
+local has_reporting = true
+
+Shared.compressed = Utils.compressed
+
+local mp_id = is_production and "404d4c479de9c3070252e692375e82ca" or "bd2099a22e4118350a46b5b360d8c4da"
+Shared.mp_id = mp_id
+local mp_url = "https://api.mixpanel.com/track"
+
+local function call_mixpanel_raw(url,data)
+	local options = {
+		method = "POST"
+		parameters = {
+			data = json_string(data)
+			verbose = "1"
+		}
+	}
+	-- logger.info(options.parameters.data)
+	local result = uri(url,options).read_text()
+	result = json_parse(result)
+	result.error and logger.error("mixpanel: "..result.error)
+end
+Shared.call_mixpanel_raw = call_mixpanel_raw
+
+function Shared.call_mixpanel(data)
+	if is_list(data) then
+		for _, event in ipairs(data) do
+			event.properties.token = mp_id
+		end
+	else
+		data.properties.token = mp_id
+	end
+	call_mixpanel_raw(mp_url,data)
+end
+
+
+local function remove_from_path(name)
+	local path = Http.request.path
+	if path == "/index.html" then
+		path = "/"
+	end
+	local params = Http.request.parameters
+	params[name] = nil
+	if not is_empty(params) then
+		local t = {}
+		for name, value in pairs(params) do
+			t[#t+1] = url_encode(name).."="..url_encode(value)
+		end
+		path = path.."?"..concat(t,"&")
+	end
+	return path
+end
+
+local function min_head(user)
+%>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<link rel="icon" href="/images/favicon.png" type="image/png">
+		<style>
+			@import "https://fonts.googleapis.com/css?family=Bitter";
+			@import "/site.css?s=<%=started%>";
+		</style>
+		<script src="/site.js?s=<%=started%>"></script>
+<%
+	local theme_date = user and user.theme_date
+	if theme_date ~= nil then
+%>
+		<style> @import "/theme.css?user=<%=user.id%>&date=<%=theme_date%>&s=<%=started%>"; </style>
+<%
+	end
+end
+
+function Shared.head()
+	local user = current_user()
+	min_head(user)
+	local source = Http.request.parameters.source
+	if source ~= nil then
+		local path = remove_from_path('source')
+%>
+		<script>
+			history.replaceState(null,null,'<%=path%>');
+		</script>
+<%
+	end
+	if user == nil and Http.request.cookies.source == nil then
+		source = source or ""
+%>
+		<script>
+			mixpanel.ours.identify();
+			mixpanel.ours.people.set({'source':'<%=source%>'});
+		</script>
+<%
+		Http.response.set_persistent_cookie( "source", source )
+	end
+%>
+		<style>
+			@import "/uploadcare/croppr.css";
+			@import "/uploadcare/uploadcare.css?s=<%=started%>";
+			@import "/dad.css";
+			@import "/admin.css?s=<%=started%>";
+		</style>
+		<script src="/uploadcare/croppr.js"></script>
+		<script src="/uploadcare/uploadcare.js?s=<%=started%>"></script>
+		<script> window.uploadcarePubKey = '<%=uploadcare_public_key%>'; </script>
+		<script src="/dad.js?v1"></script>
+<%	if user ~= nil then %>
+		<script>
+			window.UserEmail = '<%= user.email or error() %>';
+			window.UserName = '<%= user.name or error() %>';
+		</script>
+<%	end %>
+		<script src="/admin.js?s=<%=started%>"></script>
+<%
+	ab_test_head()
+end
+
+function Shared.pub_head(user)
+%>
+		<script>
+			window.Owner = '<%= user.name %>';
+<%	if user.mp_id ~= nil then %>
+			window.OwnerMpId = <%= json_string(user.mp_id) %>;
+<%	end %>
+		</script>
+<%
+	min_head(user)
+	if has_reporting then
+%>
+		<script async src="/reporting.js?s=<%=started%>"></script>
+<%
+	end
+end
+
+function Shared.page_header()
+%>
+			<a header href="/">
+				<img logo=big src="/images/logo.png">
+				<img logo=small src="/images/small_logo.png">
+			</a>
+<%
+end
+
+function Shared.body_header()
+	local user = current_user()
+%>
+		<div header>
+			<a left href="/">
+				<img logo=big src="/images/logo.png">
+				<img logo=small src="/images/small_logo.png">
+			</a>
+<%	if user == nil then %>
+			<span right login>
+				<a button login href="/login.html">Log in</a>
+				<a button register href="/register.html">Sign up</a>
+			</span>
+<%	else %>
+			<span right pulldown>
+				<img src="<%= user.get_pic_url() or "/images/user.png" %>" onclick="clickMenu(this)">
+				<div pulldown_menu>
+					<a href="javascript:copyLink()">
+						<span>linkmy.style/<%=user.name%></span>
+						<span copy>Copy</span>
+					</a>
+					<a href="/<%=user.name%>">My page</a>
+					<a href="/account.html">My account</a>
+					<a href="/links.html">Links</a>
+					<a href="/pics.html">Photos</a>
+					<a href="/theme.html">My theme</a>
+					<a href="/qr_code.html">QR code</a>
+					<a href="/analytics.html">Analytics</a>
+					<a href="javascript:logout()">Log out</a>
+				</div>
+			</span>
+			<input clipboard value="<%=base_url()%>/<%=user.name%>">
+<%	end %>
+		</div>
+<%
+end
+
+function Shared.footer()
+%>
+		<div footer>
+			<span>
+				<a href="/help.html">Help</a>
+				<br>support@linkmy.style
+			</span>
+			<span>
+				<a href="https://apps.apple.com/us/app/linkmystyle/id6475133762"><img ios src="/images/ios.svg"></a>
+				<a href="https://www.instagram.com/linkmy.style/"><img src="/images/icons/instagram.svg"></a>
+			</span>
+		</div>
+<%
+end
+
+function Shared.show_editable_link(link)
+%>
+			<div link="<%=link.id%>">
+				<div>
+					<a link pub_content href="<%=html_encode(link.url)%>" draggable=false><%=html_encode(link.title)%></a>
+					<img src="/images/drag_indicator.svg">
+				</div>
+				<button type=button small onclick="editLink('<%=link.id%>')">Edit</button>
+				<button type=button small onclick="deleteLink1(this,'<%=link.id%>')">Delete</button>
+			</div>
+<%
+end
+
+function Shared.show_saved(close_url)
+%>
+		<div saved>
+			<p>Your changes have been saved.</p>
+			<p><a href="/theme.html">Edit Theme</a></p>
+			<a close href="<%=close_url%>"><img src="/images/close.svg"></a>
+		</div>
+<%
+end
+
+function Shared.show_user_icons(user)
+	local icons = get_user_icons(user.id)
+%>
+				<div list>
+<%
+	for _, icon in ipairs(icons) do
+%>
+					<span icon="<%=icon.id%>">
+						<img src="/images/drag_indicator.svg">
+						<a <%=icon.title_attr()%> href="<%=html_encode(icon.url)%>" draggable=false>
+							<img src="/images/icons/<%=icon.name%>.svg" draggable=false>
+						</a>
+					</span>
+<%
+	end
+%>
+				</div>
+				<button big onclick="ajax('/edit_icons.js')"><%= #icons==0 and "Add" or "Edit" %></button>
+<%
+end
+
+function Shared.js_error(field,message)
+%>
+	showError( context.form, '<%=field%>', <%=json_string(message)%> );
+<%
+end
+
+local send_mail = Mail.sender(Config.mail_server).send
+
+function Shared.send_mail_async(mail)
+	thread_run( function()
+		for i in range(1,5) do
+			try
+				send_mail(mail)
+				return
+			catch e
+				logger.error("send_mail_async fail "..i..":\n"..e)
+				sleep(10000)
+			end
+		end
+		error "send_mail_async failed"
+	end )
+end
+
+Shared.theme_fields = {
+	background_color = "#c0d1d6"
+	link_background_color = "#325762"
+	link_hover_background_color = "#243f47"
+	link_text_color = "#ffffff"
+	title_color = "#000000"
+	bio_color = "#000000"
+	icon_color = ""
+	link_border_radius = "12px / 50%"
+	link_border_color = ""
+	link_shadow = ""
+	link_shadow_color = ""
+	font = "Bitter"
+	font_url = ""
+	background_img_uuid = ""
+	background_img_filename = ""
+}
+
+function Shared.password_input(value)
+	value = value or ""
+%>
+		<div password>
+			<input type=password required name=password value="<%=html_encode(value)%>" placeholder="Password">
+			<img show src="/images/visibility.svg" onclick="showPassword(parentNode)">
+			<img hide src="/images/visibility_off.svg" onclick="hidePassword(parentNode)">
+		</div>
+<%
+end
+
+function Shared.get_hashtags_list(pics)
+	local hashtags_set = {}
+	for _, pic in ipairs(pics) do
+		local hashtags = pic.hashtags
+		if hashtags ~= nil then
+			hashtags = to_list(hashtags)
+			for _, hashtag in ipairs(hashtags) do
+				hashtags_set[hashtag] = true
+			end
+			local classes = concat( hashtags, " " )
+			pic.class = [[class="]]..classes..[["]]
+		end
+	end
+	local list = {}
+	for hashtag, v in pairs(hashtags_set) do
+		if v == true then
+			list[#list+1] = hashtag
+		end
+	end
+	return list, hashtags_set
+end
+
+return Shared
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Uploadcare.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,109 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local parse = Luan.parse or error()
+local stringify = Luan.stringify or error()
+local Io = require "luan:Io.luan"
+local uri = Io.uri or error()
+local Parsers = require "luan:Parsers.luan"
+local json_parse = Parsers.json_parse or error()
+local Time = require "luan:Time.luan"
+local time_now = Time.now or error()
+local parse_time = Time.parse or error()
+local Utils = require "site:/lib/Utils.luan"
+local list_to_set = Utils.list_to_set or error()
+local Config = require "site:/private/Config.luan"
+local keys = Config.uploadcare or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "Uploadcare"
+
+
+local Uploadcare = {}
+
+function Uploadcare.url(uuid)
+	uuid or error()
+	return "https://ucarecdn.com/"..uuid.."/-/quality/smart/"
+end
+
+function Uploadcare.thumb_url(uuid)
+	uuid or error()
+	return "https://ucarecdn.com/"..uuid.."/-/crop/1:1/-/preview/400x400/-/quality/smart/"
+end
+
+local function used_uuids()
+	local list = {}
+
+	local Pic = require "site:/lib/Pic.luan"
+	local pics = Pic.search("type:pic")
+	for _, pic in ipairs(pics) do
+		list[#list+1] = pic.uuid
+	end
+
+	local User = require "site:/lib/User.luan"
+	local users = User.search("type:user")
+	for _, user in ipairs(users) do
+		list[#list+1] = user.pic_uuid
+		local data = user.theme_data
+		if data ~= nil then
+			data = parse(data)
+			list[#list+1] = data.background_img_uuid
+		end
+	end
+
+	return list_to_set(list)
+end
+
+local headers = {
+	Accept = "application/vnd.uploadcare-v0.7+json"
+	Authorization = "Uploadcare.Simple "..keys.public_key..":"..keys.secret_key
+}
+
+local delete_options = {
+	method = "DELETE"
+	headers = headers
+}
+
+local function delete(uuid)
+	local url = "https://api.uploadcare.com/files/"..uuid.."/storage/"
+	local result = uri(url,delete_options).read_text()
+end
+
+local options = {
+	headers = headers
+}
+
+local date_ptn = "yyyy-MM-dd'T'HH:mm:ss"
+local day = Time.period{days=1}
+
+function Uploadcare.gc()
+	logger.warn "start gc"
+	local used = used_uuids()
+	local count = 0
+	local url = "https://api.uploadcare.com/files/?limit=1000"
+	while url ~= nil do
+		local result = uri(url,options).read_text()
+		result = json_parse(result)
+		-- logger.info(stringify(result))
+		local results = result.results or error()
+		local now = time_now()
+		for _, r in ipairs(results) do
+			count = count + 1
+			local uuid = r.uuid or error()
+			local datetime_uploaded = r.datetime_uploaded or error()
+			local date = parse_time( date_ptn, datetime_uploaded, "GMT" )
+			if used[uuid] then
+				-- logger.info(uuid.." is in use")
+			elseif now - date < day  then
+				logger.info(uuid.." is less than a day old")
+			else
+				logger.warn(uuid.." delete")
+				delete(uuid)
+			end
+		end
+		url = result.next
+	end
+	logger.info("count = "..count)
+	logger.warn "end gc"
+end
+
+return Uploadcare
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/User.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,180 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local range = Luan.range or error()
+local to_string = Luan.to_string or error()
+local get_local_only = Luan.get_local_only or error()
+local set_local_only = Luan.set_local_only or error()
+local String = require "luan:String.luan"
+local regex = String.regex or error()
+local Math = require "luan:Math.luan"
+local random = Math.random or error()
+local Table = require "luan:Table.luan"
+local concat = Table.concat or error()
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Lucene = require "luan:lucene/Lucene.luan"
+local lucene_quote = Lucene.quote or error()
+local Http = require "luan:http/Http.luan"
+local Db = require "site:/lib/Db.luan"
+local Utils = require "site:/lib/Utils.luan"
+local long_or_nil = Utils.long_or_nil or error()
+local Uploadcare = require "site:/lib/Uploadcare.luan"
+local uploadcare_url = Uploadcare.url or error()
+local uploadcare_thumb_url = Uploadcare.thumb_url or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "User"
+
+
+local User = {}
+
+local function from_doc(doc)
+	doc.type == "user" or error "wrong type"
+	return User.new {
+		id = doc.id
+		email = doc.user_email
+		password = doc.password
+		name = doc.user_name
+		code = doc.code
+		registered = doc.user_registered
+		new_email = doc.new_email
+		pic_uuid = doc.pic_uuid
+		pic_filename = doc.pic_filename
+		title = doc.title
+		bio = doc.bio
+		theme_data = doc.theme_data
+		theme_date = doc.theme_date
+		mp_id = doc.mp_id
+		source = doc.source or doc.seller
+	}
+end
+
+local function to_doc(user)
+	return {
+		type = "user"
+		id = user.id
+		user_email = user.email
+		password = user.password
+		user_name = user.name
+		code = user.code
+		user_registered = long_or_nil(user.registered)
+		new_email = user.new_email
+		pic_uuid = user.pic_uuid
+		pic_filename = user.pic_filename
+		title = user.title
+		bio = user.bio
+		theme_data = user.theme_data
+		theme_date = long_or_nil(user.theme_date)
+		mp_id = user.mp_id
+		source = user.source
+	}
+end
+
+function User.new(user)
+
+	function user.save()
+		local doc = to_doc(user)
+		Db.save(doc)
+		user.id = doc.id
+	end
+
+	function user.reload()
+		return User.get_by_id(user.id) or error(user.id)
+	end
+
+	function user.delete()
+		Db.run_in_transaction( function()
+			local id = user.id
+			Db.delete("icon_user_id:"..id)
+			Db.delete("link_user_id:"..id)
+			Db.delete("pic_user_id:"..id)
+			Db.delete("id:"..id)
+		end )
+	end
+
+	function user.login()
+		local id = to_string(user.id)
+		Http.response.set_persistent_cookie("user",id)
+		Http.response.set_persistent_cookie("password",user.password)
+		Http.request.cookies.user = id
+		Http.request.cookies.password = user.password or error()
+	end
+
+	function user.html_title()
+		return html_encode(user.title or user.name)
+	end
+
+	function user.get_pic_url()
+		local pic_uuid = user.pic_uuid
+		return pic_uuid and uploadcare_thumb_url(pic_uuid)
+	end
+
+	return user
+end
+
+local function get_by_id(id)
+	local doc = Db.get_document("id:"..id)
+	return doc and doc.type=="user" and from_doc(doc) or nil
+end
+User.get_by_id = get_by_id
+
+function User.get_by_email(email)
+	local doc = Db.get_document("user_email:"..lucene_quote(email))
+	return doc and from_doc(doc)
+end
+
+function User.get_by_name(name)
+	local doc = Db.get_document("user_name:"..lucene_quote(name))
+	return doc and from_doc(doc)
+end
+
+function User.search(query,sort,rows)
+	rows = rows or 1000000
+	local users = {}
+	local docs = Db.search(query,1,rows,{sort=sort})
+	for _, doc in ipairs(docs) do
+		users[#users+1] = from_doc(doc)
+	end
+	return users
+end
+
+User.name_regex = regex "^[a-zA-Z0-9_-]+$"
+
+function User.current()
+	local user = get_local_only(User,"current")
+	if user == nil then
+		local id = Http.request.cookies.user
+		local password = Http.request.cookies.password
+		if id == nil or password == nil then
+			user = "nil"
+		else
+			user = get_by_id(id)
+			if user == nil or user.registered == nil or user.password ~= password then
+				user = "nil"
+			end
+		end
+		set_local_only(User,"current",user)
+	end
+	return user ~= "nil" and user or nil
+end
+
+function User.current_required()
+	local user = User.current()
+	user or Http.response.send_redirect "/login.html"
+	return user
+end
+
+function User.new_code()
+	local t = {}
+	for _ in range(1,6) do
+		t[#t+1] = to_string(random(0,9))
+	end
+	return concat(t)
+end
+
+function User.get_background_img_url(data)
+	local uuid = data.background_img_uuid
+	return uuid and uploadcare_url(uuid)
+end
+
+return User
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Utils.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,63 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local new_error = Luan.new_error or error()
+local ipairs = Luan.ipairs or error()
+local type = Luan.type or error()
+local set_metatable = Luan.set_metatable or error()
+local Number = require "luan:Number.luan"
+local long = Number.long or error()
+local String = require "luan:String.luan"
+local regex = String.regex or error()
+local Http = require "luan:http/Http.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "Utils"
+
+
+local Utils = {}
+
+Utils.is_production = Http.domain == "linkmy.style"
+
+Utils.compressed = {compressed=true}
+
+function Utils.base_url()
+	local request = Http.request
+	return request.scheme.."://"..request.headers["Host"]
+end
+
+local set_mt = {}
+function set_mt.__index(table,key)
+	return false
+end
+
+function Utils.list_to_set(list)
+	local set = {}
+	for _, v in ipairs(list) do
+		set[v] = true
+	end
+	set_metatable(set,set_mt)
+	return set
+end
+
+function Utils.to_list(input)
+	if input == nil then
+		return {}
+	elseif type(input) == "table" then
+		return input
+	else
+		return {input}
+	end
+end
+
+function Utils.long_or_nil(i)
+	return i and long(i)
+end
+
+Utils.email_regex = regex[[^[-+~.\w]+@([-.\w]+)$]]
+
+function Utils.warn(msg)
+	local e = new_error(msg)
+	e.priority = "warn"
+	e.throw()
+end
+
+return Utils
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/main_html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,145 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local String = require "luan:String.luan"
+local contains = String.contains or error()
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Thread = require "luan:Thread.luan"
+local thread_run = Thread.run or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local pub_head = Shared.pub_head or error()
+local show_saved = Shared.show_saved or error()
+local call_mixpanel = Shared.call_mixpanel or error()
+local get_hashtags_list = Shared.get_hashtags_list or error()
+local has_facebook = not not Shared.has_facebook
+local User = require "site:/lib/User.luan"
+local Link = require "site:/lib/Link.luan"
+local get_owner_links = Link.get_owner_links or error()
+local Pic = require "site:/lib/Pic.luan"
+local get_user_pics = Pic.get_user_pics or error()
+local Icon = require "site:/lib/Icon.luan"
+local get_user_icons = Icon.get_user_icons or error()
+local Facebook = require "site:/lib/Facebook.luan"
+local fb_track_visit = Facebook.track_visit or error()
+local Reporting = require "site:/lib/Reporting.luan"
+local maybe_track_visit = Reporting.maybe_track_visit or error()
+local should_track_visit = Reporting.should_track_visit or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "main_html"
+
+
+local function ad_tracking(user)
+	local referrer = Http.request.headers.referer
+	if referrer ~= nil and not contains(referrer,"linkmy.style") and maybe_track_visit(user) then
+		thread_run( function()
+			if should_track_visit(user) then
+				call_mixpanel{
+					event = "Visit"
+					properties = {
+						distinct_id = user.email
+					}
+				}
+				if has_facebook then
+					fb_track_visit(user)
+				end
+			end
+		end )
+	end
+end
+
+return function(user)
+	ad_tracking(user)
+	local user_id = user.id
+	local links = get_owner_links(user_id)
+	local pics = get_user_pics(user_id)
+	local icons = get_user_icons(user_id)
+	local title = user.html_title()
+	local pic_url = user.get_pic_url()
+	local bio = user.bio
+	local is_saved = Http.request.parameters.saved ~= nil
+	local saved = is_saved and "&saved" or ""
+	local hashtags_list, hashtags_set = get_hashtags_list(pics)
+	Http.response.headers["Content-Type"] = "text/html; charset=utf-8"
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html main>
+	<head>
+		<title>Link My Style | <%=title%></title>
+		<style hashtag></style>
+		<script>
+			'use strict';
+
+			window.Path = 'main';
+			window.Page = 'Home';
+
+			let hashtags = <%=json_string(hashtags_set)%>;
+		</script>
+<%		pub_head(user) %>
+		<script>
+			fbTrack('trackCustom', 'View', {owner:'<%=user.name%>'});
+		</script>
+	</head>
+	<body colored onload="mainInit()">
+<%	if is_saved then
+		show_saved("/"..user.name)
+	end %>
+	<div pub_background_img></div>
+	<div pub_content>
+		<div home><a href="/?source=organic">
+			<span lms>LinkMyStyle</span>
+			<br><span small>Increase Affiliate Sales</span>
+		</a></div>
+<%	if pic_url ~= nil then %>
+		<img user src="<%=pic_url%>">
+<%	end %>
+		<h1><%=title%></h1>
+<%	if bio ~= nil then %>
+		<div bio><%=html_encode(bio)%></div>
+<%	end
+	if #icons > 0 then %>
+		<div icons>
+<%		for _, icon in ipairs(icons) do %>
+			<a <%=icon.title_attr()%> href="<%=html_encode(icon.url)%>"><img src="/images/icons/<%=icon.name%>.svg"></a>
+<%		end %>
+		</div>
+<%	end %>
+		<div links>
+<%	for _, link in ipairs(links) do %>
+			<a link href="<%=html_encode(link.url)%>"><%=html_encode(link.title)%></a>
+<%	end %>
+		</div>
+<%	if #hashtags_list > 0 then %>
+		<div hashtags>
+			<span select>
+				<select onchange="mainSelectHashtag(value)">
+					<option value="">All Photos</option>
+<%		for _, hashtag in ipairs(hashtags_list) do %>
+					<option value="<%=hashtag%>">#<%=hashtag%></option>
+<%		end %>
+				</select>
+			</span>
+		</div>
+<%	end %>
+		<div pics>
+<%	for _, pic in ipairs(pics) do
+		if pic.is_hidden then
+			continue
+		end
+		local pic_id = pic.id
+%>
+			<span id="p-<%=pic_id%>" <%= pic.class or "" %> >
+				<a <%=pic.title_attr()%> href="/pic.html?pic=<%=pic_id%><%=saved%>"><img loading=lazy src="<%=pic.get_thumb_url()%>"></a>
+			</span>
+<%	end %>
+		</div>
+	</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/links.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,324 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Table = require "luan:Table.luan"
+local concat = Table.concat or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local body_header = Shared.body_header or error()
+local footer = Shared.footer or error()
+local show_editable_link = Shared.show_editable_link or error()
+local User = require "site:/lib/User.luan"
+local Link = require "site:/lib/Link.luan"
+local get_owner_links = Link.get_owner_links or error()
+local Pic = require "site:/lib/Pic.luan"
+local Utils = require "site:/lib/Utils.luan"
+local to_list = Utils.to_list or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "links.html"
+
+
+local function hashtags(pic)
+	local hashtags = to_list(pic.hashtags)
+	for i, hashtag in ipairs(hashtags) do
+		hashtags[i] = "#"..hashtag
+	end
+	return concat(hashtags," ")
+end
+
+local function div_links(links,pic_id)
+%>
+		<div links>
+			<form add onsubmit="ajaxForm('add_link.js',this)" action="javascript:">
+<%	if pic_id ~= nil then %>
+				<input type=hidden name=pic value="<%=pic_id%>">
+<%	end %>
+				<input type=text required name=title placeholder="Title">
+				<input type=url required name=url placeholder="URL">
+				<button type=submit big>Add link</button>
+			</form>
+			<div start></div>
+<%	for _, link in ipairs(links) do
+			show_editable_link(link)
+	end %>
+		</div>
+<%
+end
+
+return function()
+	local user = User.current_required()
+	if user==nil then return end
+	local owner = user
+	local pic = Http.request.parameters.pic
+	if pic ~= nil then
+		pic = Pic.get_by_id(pic)
+		if pic == nil then
+			logger.warn("pic not found\n"..Http.request.raw_head)
+			Http.response.send_error(404)
+			return
+		end
+		pic.user_id == user.id or error()
+		owner = pic
+	end
+	local links = get_owner_links(owner.id)
+	local pic_id = pic and pic.id
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+			form[add] {
+				margin-bottom: 40px;
+			}
+			input {
+				margin-bottom: 5px;
+			}
+			input[type="url"],
+			input[type="text"] {
+				display: block;
+			}
+			div[link] {
+				margin-bottom: 20px;
+			}
+			button[small] {
+				font-size: 12px;
+			}
+			div[delete2] p {
+				margin-bottom: 5px;
+			}
+
+			div[link] > div:first-of-type {
+				border-radius: 12px / 50%;
+				margin-bottom: 5px;
+				overflow: hidden;
+				position: relative;
+				border: 1px solid #ebebeb;
+			}
+			div[link] a {
+				border-radius: initial;
+				margin-bottom: initial;
+			}
+			div[link] a:hover {
+				background-color: #243F47;
+			}
+			div[link] img {
+				position: absolute;
+				top: 0;
+				height: 100%;
+				background-color: white;
+				opacity: 0.3;
+				padding: 4px;
+				touch-action: none;
+			}
+
+<%	if pic == nil then %>
+			div[links] {
+				margin-top: 20px;
+			}
+<%	else %>
+			div[msg] {
+				margin-top: 20px;
+				margin-left: 5%;
+				color: darkgreen;
+<%		if Http.request.parameters.saved == nil then %>
+				display: none;
+<%		end %>
+			}
+			div[body] {
+				margin-top: 40px;
+				margin-bottom: 20px;
+			}
+			div[pic] img {
+				width: 100%;
+				display: block;
+				margin-bottom: 5px;
+			}
+			div[pic] form {
+				margin-top: 20px;
+			}
+			div[field] {
+				margin-top: 1px;
+				margin-bottom: 10px;
+			}
+			div[hashtags] {
+				margin-top: 20px;
+			}
+			@media (min-width: 888px) {
+				div[body] {
+					display: flex;
+				}
+				div[pic] {
+					width: 45%;
+					margin-left: 5%;
+				}
+				div[outer_links] {
+					width: 55%;
+					margin-left: 5%;
+					margin-right: 5%;
+				}
+			}
+			@media (max-width: 887px) {
+				div[pic] {
+					display: block;
+					width: 90%;
+					margin-left: auto;
+					margin-right: auto;
+					margin-bottom: 40px;
+				}
+			}
+<%	end %>
+		</style>
+		<script>
+			'use strict';
+
+			function clearAddForm() {
+				let form = document.querySelector('form[add]');
+				form.querySelector('[name="title"]').value = '';
+				form.querySelector('[name="url"]').value = '';
+			}
+
+			function deleteLink1(button,linkId) {
+				let div = button.parentNode;
+				if( div.querySelector('div[delete2]') )
+					return;
+				let html = `
+				<div delete2>
+					<p>Are you sure that you want to delete this?</p>
+					<button delete2 small onclick="deleteLink2('${linkId}')">Yes</button>
+					<button small onclick="undelete1(this)">No</button>
+				</div>
+`				;
+				div.insertAdjacentHTML( 'beforeEnd', html );
+				div.scrollIntoViewIfNeeded(false);
+			}
+			function undelete1(button) {
+				button.parentNode.outerHTML = '';
+			}
+			function deleteLink2(linkId) {
+				ajax( '/delete_link.js?link='+linkId );
+			}
+			function deletePic1(button) {
+				let div = button.parentNode;
+				if( div.querySelector('div[delete2]') )
+					return;
+				let html = `
+				<div delete2>
+					<p>Are you sure that you want to delete this?</p>
+					<button delete2 small onclick="deletePic2('<%=pic_id%>')">Yes</button>
+					<button small onclick="undelete1(this)">No</button>
+				</div>
+`				;
+				div.insertAdjacentHTML( 'beforeEnd', html );
+				div.scrollIntoViewIfNeeded(false);
+			}
+			function deletePic2() {
+				ajax( '/delete_pic.js?pic=<%=pic_id%>' );
+			}
+
+			function editLink(linkId) {
+				ajax( '/edit_link.js?link='+linkId );
+			}
+
+			function cancel(linkId) {
+				ajax( '/cancel_edit_link.js?link='+linkId );
+			}
+
+			dad.onDropped = function(event) {
+				let dragging = event.original;
+				if( iDragging === indexOf(dragging.parentNode.querySelectorAll(dropSelector),dragging) )
+					return;
+				let linkId = dragging.getAttribute('link');
+				let prev = dragging.previousElementSibling;
+				let prevId = prev && prev.getAttribute('link');
+				ajax( '/move_link.js?link='+linkId+'&prev='+prevId );
+			};
+
+			dad.whatToDrag = function(draggable) {
+				return draggable.parentNode.parentNode;
+			};
+
+			function dragInit() {
+				dropSelector = 'div[link]';
+				let items = document.querySelectorAll('div[link] img');
+				for( let i=0; i<items.length; i++ ) {
+					let item = items[i];
+					dad.setDraggable(item);
+					dad.setDropzone(item.parentNode.parentNode);
+				}
+			}
+
+<%	if pic ~= nil then %>
+			function changePic(uuid,filename) {
+				ajax( '/change_pic.js', 'pic=<%=pic.id%>&uuid=' + uuid + '&filename=' + encodeURIComponent(filename) );
+			}
+			function startChangePic() {
+				uploadcare.cropprOptions = {};
+				uploadcare.upload(changePic);
+			}
+<%	end %>
+		</script>
+	</head>
+	<body>
+	<div full>
+<%
+	body_header()
+	if pic == nil then
+%>
+		<p top>Enter your title and a URL to create a link. Once saved, you can drag-and-drop the icon on the left to change the order. The order on this page will also appear on your page.</p>
+<%
+		div_links(links,pic_id)
+	else
+%>
+		<div back>
+			<a href="/pics.html#p-<%=pic.id%>"><img src="/images/keyboard_backspace.svg"></a>
+		</div>
+		<div msg>
+			Your image has been saved.
+		</div>
+		<div body>
+			<div pic>
+				<img <%=pic.title_attr()%> src="<%=pic.get_url()%>">
+				<div>
+					<button type=button small onclick="startChangePic()">Change</button>
+					<button type=button small onclick="deletePic1(this)">Delete</button>
+				</div>
+				<div hashtags><%=pic.hashtags_html("pics.html")%></div>
+				<form onsubmit="ajaxForm('/save_pic_title.js',this)" action="javascript:">
+					<input type=hidden name=pic value="<%=pic_id%>">
+					<label>Photo title</label>
+					<div field>
+						<input type=text required name=title placeholder="Title" value="<%= html_encode(pic.title or "") %>">
+					</div>
+					<label>Hashtags</label>
+					<div field>
+						<textarea name=hashtags placeholder="#hashtag1 #hashtag2"><%= hashtags(pic) %></textarea>
+						<div error=hashtags></div>
+					</div>
+					<div field>
+						<label clickable><input type=checkbox name=visible <%=pic.is_hidden and "" or "checked"%>> Visible</label>
+					</div>
+					<button type=submit small>Save</button>
+					<div error=success></div>
+				</form>
+			</div>
+			<div outer_links>
+<%				div_links(links,pic_id) %>
+			</div>
+		</div>
+<%
+	end
+%>
+<%		footer() %>
+	</div>
+	<script> dragInit(); </script>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/log_info.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,11 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Http = require "luan:http/Http.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "log_info.js"
+
+
+return function()
+	local msg = Http.request.parameters.msg or error()
+	logger.info(msg)
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/login.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,53 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local page_header = Shared.page_header or error()
+local footer = Shared.footer or error()
+local password_input = Shared.password_input or error()
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+			div[right_of_page] {
+				background-image: url(/images/shirts.jpg);
+			}
+			div[forgot] {
+				text-align: center;
+				margin-top: 10px;
+			}
+			div[register] {
+				text-align: center;
+				margin-top: 20px;
+			}
+		</style>
+	</head>
+	<body>
+		<form page onsubmit="ajaxForm('/login.js',this)" action="javascript:">
+<%			page_header() %>
+			<div>
+				<h1>Log in</h1>
+				<input type=text required name=username placeholder="Username">
+				<div error=username></div>
+<%				password_input() %>
+				<div error=password></div>
+				<button type=submit big>Log in</button>
+				<div forgot><a href="/forgot.html">Forgot username or password?</a></div>
+				<div register>Don't have a Link My Style account? <a href="/register.html">Create one</a></div>
+			</div>
+<%			footer() %>
+		</form>
+		<div right_of_page></div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/login.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,30 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local User = require "site:/lib/User.luan"
+local Shared = require "site:/lib/Shared.luan"
+local js_error = Shared.js_error or error()
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+	local username = Http.request.parameters.username or error()
+	local password = Http.request.parameters.password or error()
+	local user = User.get_by_name(username)
+	if user == nil or user.registered == nil then
+		js_error( "username", "Username not found" )
+		return
+	end
+	if user.password ~= password then
+		js_error( "password", "Wrong password" )
+		return
+	end
+	Http.response.remove_cookie("source")
+	Http.response.remove_cookie("seller")
+	user.login()
+%>
+	clearErrors(context.form);
+	location = '/';
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/move_icon.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,26 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Http = require "luan:http/Http.luan"
+local Icon = require "site:/lib/Icon.luan"
+local User = require "site:/lib/User.luan"
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+
+
+return function()
+	local user = User.current() or error()
+	local icon_id = Http.request.parameters.icon or error()
+	local prev_id = Http.request.parameters.prev or error()
+	run_in_transaction( function()
+		local icon = Icon.get_by_id(icon_id) or error()
+		icon.user_id == user.id or error()
+		local prev
+		if prev_id == "null" then
+			prev = nil
+		else
+			prev = Icon.get_by_id(prev_id) or error()
+		end
+		icon.move_after(prev)
+		icon.save()
+	end )
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/move_link.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,26 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Http = require "luan:http/Http.luan"
+local Link = require "site:/lib/Link.luan"
+local User = require "site:/lib/User.luan"
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+
+
+return function()
+	local user = User.current() or error()
+	local link_id = Http.request.parameters.link or error()
+	local prev_id = Http.request.parameters.prev or error()
+	run_in_transaction( function()
+		local link = Link.get_by_id(link_id) or error()
+		link.user_id == user.id or error()
+		local prev
+		if prev_id == "null" then
+			prev = nil
+		else
+			prev = Link.get_by_id(prev_id) or error()
+		end
+		link.move_after(prev)
+		link.save()
+	end )
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/move_pic.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,26 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Http = require "luan:http/Http.luan"
+local Pic = require "site:/lib/Pic.luan"
+local User = require "site:/lib/User.luan"
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+
+
+return function()
+	local user = User.current() or error()
+	local pic_id = Http.request.parameters.pic or error()
+	local prev_id = Http.request.parameters.prev or error()
+	run_in_transaction( function()
+		local pic = Pic.get_by_id(pic_id) or error()
+		pic.user_id == user.id or error()
+		local prev
+		if prev_id == "null" then
+			prev = nil
+		else
+			prev = Pic.get_by_id(prev_id) or error()
+		end
+		pic.move_after(prev)
+		pic.save()
+	end )
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/opened.gif.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,17 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local String = require "luan:String.luan"
+local trim = String.trim or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "opened.gif"
+
+
+local gif = Io.uri("site:/images/1.gif").read_binary()
+
+return function()
+	local email = Http.request.parameters.email
+	logger.error(email.."\nUser-Agent: "..Http.request.headers["user-agent"])
+	Http.response.binary_writer().write(gif)
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pic.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,73 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local pub_head = Shared.pub_head or error()
+local show_saved = Shared.show_saved or error()
+local Pic = require "site:/lib/Pic.luan"
+local get_pic_by_id = Pic.get_by_id or error()
+local User = require "site:/lib/User.luan"
+local get_user_by_id = User.get_by_id or error()
+local Link = require "site:/lib/Link.luan"
+local get_owner_links = Link.get_owner_links or error()
+
+
+return function()
+	local pic = Http.request.parameters.pic or error()
+	pic = get_pic_by_id(pic)
+	if pic == nil then
+		Http.response.send_error(404)
+		return
+	end
+	local pic_title = pic.title
+	local pic_id = pic.id
+	local links = get_owner_links(pic_id)
+	local user = get_user_by_id(pic.user_id) or error()
+	local title = user.html_title()
+	local is_saved = Http.request.parameters.saved ~= nil
+	local saved = is_saved and "?saved" or ""
+	local back_path = "/"..user.name..saved
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html pic>
+	<head>
+		<script>
+			window.Page = <%=json_string(pic_title)%>;
+		</script>
+<%		pub_head(user) %>
+		<title>Link My Style | <%=title%> | <%= html_encode(pic_title) %></title>
+	</head>
+	<body colored>
+<%	if is_saved then
+		show_saved("/pic.html?pic="..pic_id)
+	end %>
+	<div pub_background_img></div>
+	<div pub_content>
+		<div back>
+			<a href="<%=back_path%>#p-<%=pic_id%>"><img src="/images/keyboard_backspace.svg"></a>
+		</div>
+		<div body>
+			<div left>
+				<img pic <%=pic.title_attr()%> src="<%=pic.get_url()%>">
+				<div hashtags><%=pic.hashtags_html(back_path)%></div>
+			</div>
+			<div outer_links>
+				<div links>
+<%	for _, link in ipairs(links) do %>
+					<a link=x href="<%=link.url%>"><%=html_encode(link.title)%></a>
+<%	end %>
+				</div>
+			</div>
+		</div>
+	</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pics.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,203 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local Table = require "luan:Table.luan"
+local concat = Table.concat or error()
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local body_header = Shared.body_header or error()
+local footer = Shared.footer or error()
+local get_hashtags_list = Shared.get_hashtags_list or error()
+local User = require "site:/lib/User.luan"
+local Pic = require "site:/lib/Pic.luan"
+local get_user_pics = Pic.get_user_pics or error()
+
+
+return function()
+	local user = User.current_required()
+	if user==nil then return end
+	local pics = get_user_pics(user.id)
+	local hashtags_list, hashtags_set = get_hashtags_list(pics)
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+			div[pics_body] {
+				width: 90%;
+				margin-left: auto;
+				margin-right: auto;
+			}
+			div[pics] > span {
+				position: relative;
+			}
+			div[pics] span > img {
+				position: absolute;
+				height: 25%;
+				top: 0;
+				left: 0;
+				background-color: white;
+				border: 1px solid lightgrey;
+				opacity: 0.3;
+				padding: 4px;
+				touch-action: none;
+			}
+			div[is_hidden] {
+				position: absolute;
+				top: 50%;
+				left: 50%;
+				transform: translate(-50%,-50%);
+				color: white;
+				background-color: black;
+				opacity: 0.5;
+				padding: 20px;
+			}
+			div[add] {
+				margin-top: 20px;
+				margin-bottom: 40px;
+				margin-left: auto;
+				margin-right: auto;
+				max-width: 600px;
+			}
+			div[add] > * {
+				margin-bottom: 8px;
+			}
+			button[big] {
+				margin-top: 5px;
+			}
+			div[hashtags] {
+				text-align: center;
+				margin-bottom: 20px;
+			}
+		</style>
+		<style hashtag></style>
+		<script>
+			'use strict';
+
+			function addPic(uuid,filename) {
+				let title = document.querySelector('input[name="title"]');
+				let visible = document.querySelector('input[name="visible"]');
+				let data = 'uuid=' + uuid + '&filename=' + encodeURIComponent(filename) + '&title=' + encodeURIComponent(title.value)
+				if( visible.checked )
+					data += '&visible=on';
+				ajax( '/add_pic.js', data );
+			}
+
+			function startAddPic() {
+				let title = document.querySelector('input[name="title"]');
+				if( !title.reportValidity() )
+					return;
+				uploadcare.cropprOptions = {};
+				uploadcare.upload(addPic);
+			}
+
+			dad.onDropped = function(event) {
+				let dragging = event.original;
+				if( iDragging === indexOf(dragging.parentNode.querySelectorAll(dropSelector),dragging) )
+					return;
+				let picId = dragging.getAttribute('pic');
+				let prev = dragging.previousElementSibling;
+				let prevId = prev && prev.getAttribute('pic');
+				ajax( '/move_pic.js?pic='+picId+'&prev='+prevId );
+			};
+
+			dad.whatToDrag = function(draggable) {
+				return draggable.parentNode;
+			};
+
+			function dragInit() {
+				dropSelector = 'span';
+				let items = document.querySelectorAll('div[pics] span > img');
+				for( let i=0; i<items.length; i++ ) {
+					let item = items[i];
+					dad.setDraggable(item);
+					dad.setDropzone(item.parentNode);
+				}
+			}
+
+			let hashtags = <%=json_string(hashtags_set)%>;
+
+			function selectHashtag(hashtag) {
+				let style = document.querySelector('style[hashtag]');
+				if( hashtags[hashtag] ) {
+					style.innerHTML = `
+						div[pics] > span {
+							display: none;
+						}
+						div[pics] > span.${hashtag} {
+							display: block;
+						}
+`					;
+					history.replaceState(null,null,`#${hashtag}`);
+				} else {
+					style.innerHTML = '';
+					history.replaceState(null,null,'pics.html');
+				}
+			}
+
+			function init() {
+				dragInit();
+				let hash = location.hash;
+				if( hash ) {
+					hash = hash.slice(1);  // remove '#'
+					if( hashtags[hash] ) {
+						let select = document.querySelector('[hashtags] select');
+						select.value = hash;
+						selectHashtag(hash);
+					}
+				}
+			}
+		</script>
+	</head>
+	<body>
+	<div full>
+<%		body_header() %>
+		<p top>Upload a photo. Once your photo has been saved, you can add links that are associated with it. Links can be outfits, products, affiliate links, the original image/video, or anything you like.  Drag-and-drop the icon on the upper-left to change the order. The order on this page will also appear on your page.</p>
+		<div pics_body>
+			<div add>
+				<input type=text required name=title placeholder="Photo title">
+				<label clickable><input type=checkbox name=visible checked> Visible</label>
+				<button type=button big onclick="startAddPic()">Add an image</button>
+			</div>
+<%	if #hashtags_list > 0 then %>
+			<div hashtags>
+				<span select>
+					<select onchange="selectHashtag(value)">
+						<option value="">All Photos</option>
+<%		for _, hashtag in ipairs(hashtags_list) do %>
+						<option value="<%=hashtag%>">#<%=hashtag%></option>
+<%		end %>
+					</select>
+				</span>
+			</div>
+<%	end %>
+			<div pics>
+<%	for _, pic in ipairs(pics) do
+		local pic_id = pic.id
+%>
+				<span pic="<%=pic_id%>" id="p-<%=pic_id%>" <%= pic.class or "" %> >
+					<a <%=pic.title_attr()%> href="/links.html?pic=<%=pic_id%>" draggable=false>
+						<img loading=lazy src="<%=pic.get_thumb_url()%>" draggable=false>
+<%		if pic.is_hidden then %>
+						<div is_hidden>Hidden</div>
+<%		end %>
+					</a>
+					<img src="/images/drag_indicator.svg">
+				</span>
+<%	end %>
+			</div>
+		</div>
+<%		footer() %>
+	</div>
+	<script> init(); </script>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/Config_sample.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,16 @@
+return {
+	mail_server = {
+		host = "smtpcorp.com"
+		port = 465
+		username = "linkmystyle"
+		password = "xxx"
+	}
+	Config.uploadcare = {
+		public_key = "962640c4b84c779bca4f"
+		secret_key = "xxx"
+	}
+	facebook = {
+		access_token = "xxx"
+	}
+	push_password = "xxx"
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/reports/analytics.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,223 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local body_header = Shared.body_header or error()
+local footer = Shared.footer or error()
+local compressed = Shared.compressed or error()
+local Reporting = require "site:/lib/Reporting.luan"
+local get_data = Reporting.get_data or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "analytics_new.html"
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+			div[content] {
+				margin-left: 3%;
+				margin-right: 3%;
+			}
+			h1 {
+				text-align: center;
+				margin-bottom: 0;
+			}
+			p[top] {
+				text-align: center;
+			}
+			div[row] {
+				display: flex;
+				align-items: flex-start;
+				justify-content: space-between;
+				margin-top: 20px;
+				margin-bottom: 20px;
+			}
+			div[report] {
+				width: 48%;
+			}
+		</style>
+		<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
+		<script>
+<%
+	do
+		local data = get_data( "+type:visit", "day" )
+%>
+			function initTraffic() {
+				let options = {
+					chart: {
+						type: 'line'
+					},
+					series: [{
+						name: 'Visitors',
+						data: <%=json_string(data,compressed)%>,
+					}],
+					xaxis: {
+						type: 'datetime'
+					},
+					title: {
+						text: 'Traffic'
+					}
+				};
+				let div = document.querySelector('div[report="traffic"]');
+				let chart = new ApexCharts( div, options );
+				chart.render();
+			}
+<%
+	end_do
+	do
+		local data = get_data( "+type:click", "day" )
+%>
+			function initClicks() {
+				let options = {
+					chart: {
+						type: 'line'
+					},
+					series: [{
+						name: 'Clicks',
+						data: <%=json_string(data,compressed)%>,
+					}],
+					xaxis: {
+						type: 'datetime'
+					},
+					title: {
+						text: 'Clicks'
+					}
+				};
+				let div = document.querySelector('div[report="clicks"]');
+				let chart = new ApexCharts( div, options );
+				chart.render();
+			}
+
+<%
+	end_do
+	do
+		local data = get_data( "+type:monthly_visit", "owner" )
+%>
+			function initOwnerTraffic() {
+				let data = <%=json_string(data,compressed)%>;
+				let options = {
+					chart: {
+						type: 'bar',
+						height: barChartHeight(data.length)
+					},
+					plotOptions: {
+						bar: {
+							horizontal: true
+						}
+					},
+					series: [{
+						name: 'Visitors',
+						data: data,
+					}],
+					title: {
+						text: 'Traffic by Owner'
+					}
+				};
+				let div = document.querySelector('div[report="owner_traffic"]');
+				let chart = new ApexCharts( div, options );
+				chart.render();
+			}
+<%
+	end_do
+	do
+		local data = get_data( "+type:monthly_click", "owner" )
+%>
+			function initOwnerClicks() {
+				let data = <%=json_string(data,compressed)%>;
+				let options = {
+					chart: {
+						type: 'bar',
+						height: barChartHeight(data.length)
+					},
+					plotOptions: {
+						bar: {
+							horizontal: true
+						}
+					},
+					series: [{
+						name: 'Clicks',
+						data: data,
+					}],
+					title: {
+						text: 'Clicks by Owner'
+					}
+				};
+				let div = document.querySelector('div[report="owner_clicks"]');
+				let chart = new ApexCharts( div, options );
+				chart.render();
+			}
+<%
+	end_do
+	do
+		local data = get_data( "+type:referrer", "value" )
+%>
+			function initReferrers() {
+				let data = <%=json_string(data,compressed)%>;
+				let options = {
+					chart: {
+						type: 'bar',
+						height: barChartHeight(data.length)
+					},
+					plotOptions: {
+						bar: {
+							horizontal: true
+						}
+					},
+					series: [{
+						name: 'Visitors',
+						data: data,
+					}],
+					title: {
+						text: 'Referring Domains'
+					}
+				};
+				let div = document.querySelector('div[report="referrers"]');
+				let chart = new ApexCharts( div, options );
+				chart.render();
+			}
+<%
+	end_do
+%>
+			function init() {
+				initTraffic();
+				initClicks();
+				initOwnerTraffic();
+				initOwnerClicks();
+				initReferrers();
+			}
+		</script>
+	</head>
+	<body onload="init()">
+	<div full>
+<%		body_header() %>
+		<div content>
+			<h1>Analytics</h1>
+			<p top>For the last 30 days</p>
+			<div row>
+				<div report=traffic></div>
+				<div report=clicks></div>
+			</div>
+			<div row>
+				<div report=owner_traffic></div>
+				<div report=owner_clicks></div>
+			</div>
+			<div row>
+				<div report=referrers></div>
+			</div>
+		</div>
+<%		footer() %>
+	</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/reports/delete_user.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,16 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local User = require "site:/lib/User.luan"
+
+
+return function()
+	local user_id = Http.request.parameters.user or error()
+	local user = User.get_by_id(user_id) or error "user not found"
+	user.delete()
+	Io.stdout = Http.response.text_writer()
+%>
+	location.reload();
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/reports/registered.txt.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,24 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Db = require "site:/lib/Db.luan"
+
+
+return function()
+	local registered = 0
+	local unregistered = 0
+	Db.advanced_search( "type:user", function(_,doc_fn,_)
+		local doc = doc_fn()
+		if doc.user_registered == nil then
+			unregistered = unregistered + 1
+		else
+			registered = registered + 1
+		end
+	end )
+	Io.stdout = Http.response.text_writer()
+%>
+<%=registered%> registered
+<%=unregistered%> unregistered
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/reports/registers.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,50 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local User = require "site:/lib/User.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "reports/registers.html"
+
+
+return function()
+	local users = User.search("type:user","id desc",100)
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			@import "/tools/tools.css";
+		</style>
+	</head>
+	<body>
+		<h1>registers</h1>
+		<table>
+			<tr>
+				<th>email</th>
+				<th>name</th>
+				<th>code</th>
+			</tr>
+<%
+		for _, user in ipairs(users) do
+			local code = user.code
+			if code == nil then
+				continue
+			end
+%>
+			<tr>
+				<td><%= user.email %></td>
+				<td><a href="/register2.html?user=<%=user.name%>"><%= user.name %></a></td>
+				<td><%= code %></td>
+			</tr>
+<%
+		end
+%>
+		</table>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/reports/save_source.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,27 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local User = require "site:/lib/User.luan"
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+
+
+return function()
+	local user_id = Http.request.parameters.user or error()
+	local source = Http.request.parameters.source or error()
+	local user
+	run_in_transaction( function()
+		user = User.get_by_id(user_id) or error()
+		user.source = source
+		user.save()
+	end )
+	Io.stdout = Http.response.text_writer()
+%>
+	let td = document.querySelector('td[user="<%=user_id%>"]');
+	let span = td.querySelector('span');
+	span.textContent = '<%=source%>';
+	let input = td.querySelector('input');
+	input.blur();
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/reports/users.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,124 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local String = require "luan:String.luan"
+local to_number = String.to_number or error()
+local Table = require "luan:Table.luan"
+local sort = Table.sort or error()
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local User = require "site:/lib/User.luan"
+local Db = require "site:/lib/Db.luan"
+local Reporting = require "site:/lib/Reporting.luan"
+local owners_with_visits = Reporting.owners_with_visits or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "reports/users.html"
+
+
+local function by_links(user1,user2)
+	return user1.links > user2.links
+end
+
+return function()
+	local n = Http.request.parameters.n
+	n = n and to_number(n)
+	local users = User.search("user_registered:*","user_registered desc",n)
+	local has_visits = owners_with_visits()
+	for _, user in ipairs(users) do
+		user.links = Db.count("link_user_id:"..user.id)
+		user.traffic = has_visits[user.id] and "yes" or ""
+	end
+	if Http.request.parameters.sort == "links" then
+		sort(users,by_links)
+	end
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			@import "/tools/tools.css";
+
+			div[source] {
+				cursor: pointer;
+			}
+		</style>
+		<script src="/site.js"></script>
+		<script src="/admin.js"></script>
+		<script>
+			function showForm(td) {
+				let divSource = td.querySelector('div[source]');
+				let span = divSource.querySelector('span');
+				let divForm = td.querySelector('div[form]');
+				let divHidden = document.querySelector('div[hidden]');
+				divSource.style.display = 'none';
+				divForm.innerHTML = divHidden.innerHTML;
+				let form = divForm.querySelector('form');
+				form.onsubmit = function(){ submitted(td); }
+				let input = divForm.querySelector('input');
+				input.value = span.textContent;
+				input.onblur = function(){ hideForm(td); }
+				input.focus();
+			}
+			function hideForm(td) {
+				let divSource = td.querySelector('div[source]');
+				let divForm = td.querySelector('div[form]');
+				divSource.style.display = 'block';
+				divForm.innerHTML = '';
+			}
+			function submitted(td) {
+				let input = td.querySelector('input');
+				let userId = td.getAttribute('user');
+				ajax( `save_source.js?user=${userId}&source=${input.value}` );
+			}
+
+			function deleteUser(userId,userName) {
+				if( !confirm(`Do you want to delete ${userName}?`) )
+					return;
+				ajax( `delete_user.js?user=${userId}` );
+				console.log('qqq');
+			}
+		</script>
+	</head>
+	<body>
+		<h1>recent users</h1>
+		<table>
+			<tr>
+				<th><a href="?">registered</a></th>
+				<th><a href="?sort=links">links</a></th>
+				<th>traffic</th>
+				<th>name</th>
+				<th>password</th>
+				<th>email</th>
+				<th>title</th>
+				<th></th>
+			</tr>
+<%
+		for _, user in ipairs(users) do
+%>
+			<tr>
+				<td><script>date(<%= user.registered %>)</script></td>
+				<td align=right><%= user.links %></td>
+				<td><%= user.traffic %></td>
+				<td><a href="/<%=user.name%>"><%=user.name%></a></td>
+				<td><%= html_encode(user.password) %></td>
+				<td><%= user.email %></td>
+				<td><%= html_encode(user.title or user.name) %></td>
+				<td><button onclick="deleteUser(<%=user.id%>,'<%=user.name%>')">delete</button></td>
+			</tr>
+<%
+		end
+%>
+		</table>
+		<div hidden=form>
+			<form action="javascript:">
+				<input>
+			</form>
+		</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/reports/users.json.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,26 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local User = require "site:/lib/User.luan"
+local Db = require "site:/lib/Db.luan"
+
+
+local function by_links(user1,user2)
+	return user1.links > user2.links
+end
+
+return function()
+	local users = User.search("user_registered:*")
+	local emails = {}
+	for _, user in ipairs(users) do
+		if Db.count("link_user_id:"..user.id) > 0 then
+			emails[#emails+1] = user.email
+		end
+	end
+	Io.stdout = Http.response.text_writer()
+	%><%= json_string(emails) %><%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/tools/email.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,165 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			@import "/tools/tools.css";
+
+			body {
+				margin: 3%;
+			}
+			textarea {
+				width: 100%;
+			}
+			textarea[name=server] {
+				height: 130px;
+			}
+			textarea[name=mail] {
+				height: 260px;
+			}
+		</style>
+		<script>
+			let mails = {};
+
+			mails.chatty_text = `
+{
+	To = "ftest@linkmy.style"
+	From = "Link My Style <support@linkmy.style>"
+	Subject = "checking in"
+	["MIME-Version"] = "1.0"
+	body = [[
+Hi there,
+
+just checking if this email reaches you.
+
+Have a great day.
+
+mlrch-6c2977f34d1e0
+]]
+}
+`			.trim();
+
+			mails.chatty_multipart = `
+{
+	To = "ftest@linkmy.style"
+	From = "Link My Style <support@linkmy.style>"
+	Subject = "checking in"
+	["MIME-Version"] = "1.0"
+	["Content-Type"] = "multipart/alternative"
+	body = {
+		{
+			["Content-Type"] = [[text/plain; charset="UTF-8"]]
+			body = [[
+Hi there,
+
+just checking if this email reaches you.
+
+Have a great day.
+]]
+		}
+		{
+			["Content-Type"] = [[text/html; charset="UTF-8"]]
+			body = [[
+Hi there,<br>
+<br>
+just checking if this email reaches you.<br>
+<br mailreach="mlrch-6c2977f34d1e0">
+Have a great day.<br>
+]]
+		}
+	}
+}
+`			.trim();
+
+			mails.register_text = `
+{
+	To = "ftest@linkmy.style"
+	From = "Link My Style <support@linkmy.style>"
+	Subject = "Confirmation Code"
+	["MIME-Version"] = "1.0"
+	body = [[
+Thank you for registering.  Please use the 6 digit confirmation code below to complete the process:
+
+Confirmation Code: 999999
+
+If you did not request this code, please ignore this email.
+
+mlrch-6c2977f34d1e0
+]]
+}
+`			.trim();
+
+			mails.register_multipart = `
+{
+	To = "ftest@linkmy.style"
+	From = "Link My Style <support@linkmy.style>"
+	Subject = "Confirmation Code"
+	["MIME-Version"] = "1.0"
+	["Content-Type"] = "multipart/alternative"
+	body = {
+		{
+			["Content-Type"] = [[text/plain; charset="UTF-8"]]
+			body = [[
+Thank you for registering.  Please use the 6 digit confirmation code below to complete the process:
+
+Confirmation Code: 999999
+
+If you did not request this code, please ignore this email.
+]]
+		}
+		{
+			["Content-Type"] = [[text/html; charset="UTF-8"]]
+			body = [[
+Thank you for registering.  Please use the 6 digit confirmation code below to complete the process:<br>
+<br>
+Confirmation Code: <b>999999</b><br>
+<br mailreach="mlrch-6c2977f34d1e0">
+If you did not request this code, please ignore this email.<br>
+]]
+		}
+	}
+}
+`			.trim();
+
+			function set(what) {
+				document.querySelector('textarea[name=mail]').value = mails[what];
+			}
+		</script>
+		<title>Test Email</title>
+	</head>
+	<body>
+		<h1>Test Email</h1>
+
+		<form method=post action="send_email.txt">
+			<p>
+				SMTP Server<br>
+				<textarea name=server>
+{
+	host = "smtpcorp.com"
+	port = 465
+	username = "linkmystyle"
+	password = "xxx"
+}
+</textarea>
+			</p>
+			<p>
+				Mail<br>
+				<textarea name=mail></textarea><br>
+				<button type=button onclick="set('chatty_text')">chatty_text</button>
+				<button type=button onclick="set('chatty_multipart')">chatty_multipart</button>
+				<button type=button onclick="set('register_text')">register_text</button>
+				<button type=button onclick="set('register_multipart')">register_multipart</button>
+			</p>
+			<p>
+				<input type=submit>
+			</p>
+		</form>
+
+		<p>The textareas contain <a href="https://www.luan.software/">Luan</a> tables which is more convenient than JSON.  Table keys can be like <b>key</b> when the key is like a variable name, or like <b>["a-key"]</b> when the key contains characters that aren't allowed in variable names.  String values can be like <b>"..."</b> or like <b>[[...]]</b> which can be multi-line.</b>
+
+		<p>Change the server password to make this work</p>
+
+		<p>Note that all mail fields except <b>body</b> are simply passed directly to SMTP.</p>
+	</body>
+	<script> set('register_multipart'); </script>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/tools/error.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,16 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<script src=/site.js></script>
+		<script>
+			function err() {
+				//qqq();
+				ajax('/qqq');
+			}
+		</script>
+	</head>
+	<body>
+		<button onclick="err()">error</button>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/tools/lucene.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,6 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Web_search = require "luan:lucene/Web_search.luan"
+local Db = require "site:/lib/Db.luan"
+
+return Web_search.of(Db)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/tools/reporting.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,6 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Web_search = require "luan:lucene/Web_search.luan"
+local Reporting = require "site:/lib/Reporting.luan"
+
+return Web_search.of(Reporting.db)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/tools/run.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,1 @@
+return require("luan:http/tools/Run.luan").respond
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/tools/send_email.txt.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,17 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local parse = Luan.parse or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Mail = require "luan:mail/Mail.luan"
+
+
+return function()
+	local server = Http.request.parameters.server or error "parameter 'server' missing"
+	local mail = Http.request.parameters.mail or error "parameter 'mail' missing"
+	server = parse(server)
+	mail = parse(mail)
+	Mail.sender(server).send(mail)
+	Io.stdout = Http.response.text_writer()
+	%>sent<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/tools/shell.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,1 @@
+return require("luan:http/tools/Shell.luan").respond
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/tools/tools.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,37 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			@import "/tools/tools.css";
+		</style>
+	</head>
+	<body>
+		<h1>Local Tools</h1>
+		<p><a href="/">home</a></p>
+		<p>
+			reports:
+			<a href="../reports/users.html?n=100">recent users</a>
+			- <a href="../reports/registers.html">registers</a>
+			- <a href="../reports/registered.txt">registered</a>
+			- <a href="../reports/analytics.html">analytics</a>
+			- <a href="../reports/users.json">users.json</a>
+		</p>
+		<p>
+			public tools:
+			<a href="/tools/dimensions.html">dimensions</a>
+			- <a href="/tools/cookies.html">cookies</a>
+			- <a href="/tools/request.txt">HTTP request</a>
+		</p>
+		<p><a href="email.html">email test</a></p>
+		<p><a href="../local/logs/">logs</a></p>
+		<p><a href="../rev.txt">hg rev</a></p>
+		<p>
+			<a href="lucene.html">lucene</a>
+			- <a href="reporting.html">reporting</a>
+		</p>
+		<p><a href="shell.html">luan shell</a></p>
+		<p><a href="run">luan batch</a></p>
+		<p><a href="/uploadcare/">uploadcare</a></p>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/private/tools/uploadcare_gc.txt.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,15 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Thread = require "luan:Thread.luan"
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Uploadcare = require "site:/lib/Uploadcare.luan"
+
+
+return function()
+	Thread.run(function()
+		Uploadcare.gc()
+	end)
+	Io.stdout = Http.response.text_writer()
+	%>in log<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/qr_code.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,68 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Utils = require "site:/lib/Utils.luan"
+local base_url = Utils.base_url or error()
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local body_header = Shared.body_header or error()
+local footer = Shared.footer or error()
+local User = require "site:/lib/User.luan"
+
+
+return function()
+	local user = User.current_required()
+	if user==nil then return end
+	local url = base_url().."/"..user.name
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+			div[body] {
+				text-align: center;
+			}
+			[qrcode] {
+				display: inline-block;
+			}
+			a[download] {
+				display: inline-block !important;
+				width: 256px !important;
+			}
+		</style>
+		<script type="text/javascript" src="qrcode.js"></script>
+	</head>
+	<body onload="setHref()">
+	<div full>
+<%		body_header() %>
+		<div body>
+			<h1>QR Code</h1>
+			<span qrcode></span>
+			<p><a download="linkmystyle" button big>Download</a></p>
+		</div>
+<%		footer() %>
+	</div>
+	</body>
+	<script>
+		'use strict';
+
+		let qrcode = new QRCode( document.querySelector('[qrcode]'), {
+			text: '<%=url%>',
+			width: 256,
+			height: 256,
+		} );
+		//console.log(qrcode);
+		function setHref() {
+			let a = document.querySelector('a[download]');
+			let img = document.querySelector('[qrcode] img');
+			//console.log(img.src);
+			a.href = img.src;
+		}
+	</script>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/qrcode.js	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,614 @@
+/**
+ * @fileoverview
+ * - Using the 'QRCode for Javascript library'
+ * - Fixed dataset of 'QRCode for Javascript library' for support full-spec.
+ * - this library has no dependencies.
+ * 
+ * @author davidshimjs
+ * @see <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a>
+ * @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a>
+ */
+var QRCode;
+
+(function () {
+	//---------------------------------------------------------------------
+	// QRCode for JavaScript
+	//
+	// Copyright (c) 2009 Kazuhiko Arase
+	//
+	// URL: http://www.d-project.com/
+	//
+	// Licensed under the MIT license:
+	//   http://www.opensource.org/licenses/mit-license.php
+	//
+	// The word "QR Code" is registered trademark of 
+	// DENSO WAVE INCORPORATED
+	//   http://www.denso-wave.com/qrcode/faqpatent-e.html
+	//
+	//---------------------------------------------------------------------
+	function QR8bitByte(data) {
+		this.mode = QRMode.MODE_8BIT_BYTE;
+		this.data = data;
+		this.parsedData = [];
+
+		// Added to support UTF-8 Characters
+		for (var i = 0, l = this.data.length; i < l; i++) {
+			var byteArray = [];
+			var code = this.data.charCodeAt(i);
+
+			if (code > 0x10000) {
+				byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18);
+				byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12);
+				byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6);
+				byteArray[3] = 0x80 | (code & 0x3F);
+			} else if (code > 0x800) {
+				byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12);
+				byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6);
+				byteArray[2] = 0x80 | (code & 0x3F);
+			} else if (code > 0x80) {
+				byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6);
+				byteArray[1] = 0x80 | (code & 0x3F);
+			} else {
+				byteArray[0] = code;
+			}
+
+			this.parsedData.push(byteArray);
+		}
+
+		this.parsedData = Array.prototype.concat.apply([], this.parsedData);
+
+		if (this.parsedData.length != this.data.length) {
+			this.parsedData.unshift(191);
+			this.parsedData.unshift(187);
+			this.parsedData.unshift(239);
+		}
+	}
+
+	QR8bitByte.prototype = {
+		getLength: function (buffer) {
+			return this.parsedData.length;
+		},
+		write: function (buffer) {
+			for (var i = 0, l = this.parsedData.length; i < l; i++) {
+				buffer.put(this.parsedData[i], 8);
+			}
+		}
+	};
+
+	function QRCodeModel(typeNumber, errorCorrectLevel) {
+		this.typeNumber = typeNumber;
+		this.errorCorrectLevel = errorCorrectLevel;
+		this.modules = null;
+		this.moduleCount = 0;
+		this.dataCache = null;
+		this.dataList = [];
+	}
+
+	QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);}
+	return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row<this.moduleCount;row++){this.modules[row]=new Array(this.moduleCount);for(var col=0;col<this.moduleCount;col++){this.modules[row][col]=null;}}
+	this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(test,maskPattern);if(this.typeNumber>=7){this.setupTypeNumber(test);}
+	if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);}
+	this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}}
+	return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row<this.modules.length;row++){var y=row*cs;for(var col=0;col<this.modules[row].length;col++){var x=col*cs;var dark=this.modules[row][col];if(dark){qr_mc.beginFill(0,100);qr_mc.moveTo(x,y);qr_mc.lineTo(x+cs,y);qr_mc.lineTo(x+cs,y+cs);qr_mc.lineTo(x,y+cs);qr_mc.endFill();}}}
+	return qr_mc;},setupTimingPattern:function(){for(var r=8;r<this.moduleCount-8;r++){if(this.modules[r][6]!=null){continue;}
+	this.modules[r][6]=(r%2==0);}
+	for(var c=8;c<this.moduleCount-8;c++){if(this.modules[6][c]!=null){continue;}
+	this.modules[6][c]=(c%2==0);}},setupPositionAdjustPattern:function(){var pos=QRUtil.getPatternPosition(this.typeNumber);for(var i=0;i<pos.length;i++){for(var j=0;j<pos.length;j++){var row=pos[i];var col=pos[j];if(this.modules[row][col]!=null){continue;}
+	for(var r=-2;r<=2;r++){for(var c=-2;c<=2;c++){if(r==-2||r==2||c==-2||c==2||(r==0&&c==0)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}}}},setupTypeNumber:function(test){var bits=QRUtil.getBCHTypeNumber(this.typeNumber);for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;}
+	for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}}
+	for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}}
+	this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex<data.length){dark=(((data[byteIndex]>>>bitIndex)&1)==1);}
+	var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;}
+	this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}}
+	row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;i<dataList.length;i++){var data=dataList[i];buffer.put(data.mode,4);buffer.put(data.getLength(),QRUtil.getLengthInBits(data.mode,typeNumber));data.write(buffer);}
+	var totalDataCount=0;for(var i=0;i<rsBlocks.length;i++){totalDataCount+=rsBlocks[i].dataCount;}
+	if(buffer.getLengthInBits()>totalDataCount*8){throw new Error("code length overflow. ("
+	+buffer.getLengthInBits()
+	+">"
+	+totalDataCount*8
+	+")");}
+	if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);}
+	while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);}
+	while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;}
+	buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;}
+	buffer.put(QRCodeModel.PAD1,8);}
+	return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r<rsBlocks.length;r++){var dcCount=rsBlocks[r].dataCount;var ecCount=rsBlocks[r].totalCount-dcCount;maxDcCount=Math.max(maxDcCount,dcCount);maxEcCount=Math.max(maxEcCount,ecCount);dcdata[r]=new Array(dcCount);for(var i=0;i<dcdata[r].length;i++){dcdata[r][i]=0xff&buffer.buffer[i+offset];}
+	offset+=dcCount;var rsPoly=QRUtil.getErrorCorrectPolynomial(ecCount);var rawPoly=new QRPolynomial(dcdata[r],rsPoly.getLength()-1);var modPoly=rawPoly.mod(rsPoly);ecdata[r]=new Array(rsPoly.getLength()-1);for(var i=0;i<ecdata[r].length;i++){var modIndex=i+modPoly.getLength()-ecdata[r].length;ecdata[r][i]=(modIndex>=0)?modPoly.get(modIndex):0;}}
+	var totalCodeCount=0;for(var i=0;i<rsBlocks.length;i++){totalCodeCount+=rsBlocks[i].totalCount;}
+	var data=new Array(totalCodeCount);var index=0;for(var i=0;i<maxDcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<dcdata[r].length){data[index++]=dcdata[r][i];}}}
+	for(var i=0;i<maxEcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<ecdata[r].length){data[index++]=ecdata[r][i];}}}
+	return data;};var QRMode={MODE_NUMBER:1<<0,MODE_ALPHA_NUM:1<<1,MODE_8BIT_BYTE:1<<2,MODE_KANJI:1<<3};var QRErrorCorrectLevel={L:1,M:0,Q:3,H:2};var QRMaskPattern={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var QRUtil={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:(1<<10)|(1<<8)|(1<<5)|(1<<4)|(1<<2)|(1<<1)|(1<<0),G18:(1<<12)|(1<<11)|(1<<10)|(1<<9)|(1<<8)|(1<<5)|(1<<2)|(1<<0),G15_MASK:(1<<14)|(1<<12)|(1<<10)|(1<<4)|(1<<1),getBCHTypeInfo:function(data){var d=data<<10;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)>=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));}
+	return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));}
+	return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;}
+	return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i<errorCorrectLength;i++){a=a.multiply(new QRPolynomial([1,QRMath.gexp(i)],0));}
+	return a;},getLengthInBits:function(mode,type){if(1<=type&&type<10){switch(mode){case QRMode.MODE_NUMBER:return 10;case QRMode.MODE_ALPHA_NUM:return 9;case QRMode.MODE_8BIT_BYTE:return 8;case QRMode.MODE_KANJI:return 8;default:throw new Error("mode:"+mode);}}else if(type<27){switch(mode){case QRMode.MODE_NUMBER:return 12;case QRMode.MODE_ALPHA_NUM:return 11;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 10;default:throw new Error("mode:"+mode);}}else if(type<41){switch(mode){case QRMode.MODE_NUMBER:return 14;case QRMode.MODE_ALPHA_NUM:return 13;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 12;default:throw new Error("mode:"+mode);}}else{throw new Error("type:"+type);}},getLostPoint:function(qrCode){var moduleCount=qrCode.getModuleCount();var lostPoint=0;for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount;col++){var sameCount=0;var dark=qrCode.isDark(row,col);for(var r=-1;r<=1;r++){if(row+r<0||moduleCount<=row+r){continue;}
+	for(var c=-1;c<=1;c++){if(col+c<0||moduleCount<=col+c){continue;}
+	if(r==0&&c==0){continue;}
+	if(dark==qrCode.isDark(row+r,col+c)){sameCount++;}}}
+	if(sameCount>5){lostPoint+=(3+sameCount-5);}}}
+	for(var row=0;row<moduleCount-1;row++){for(var col=0;col<moduleCount-1;col++){var count=0;if(qrCode.isDark(row,col))count++;if(qrCode.isDark(row+1,col))count++;if(qrCode.isDark(row,col+1))count++;if(qrCode.isDark(row+1,col+1))count++;if(count==0||count==4){lostPoint+=3;}}}
+	for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount-6;col++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row,col+1)&&qrCode.isDark(row,col+2)&&qrCode.isDark(row,col+3)&&qrCode.isDark(row,col+4)&&!qrCode.isDark(row,col+5)&&qrCode.isDark(row,col+6)){lostPoint+=40;}}}
+	for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount-6;row++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row+1,col)&&qrCode.isDark(row+2,col)&&qrCode.isDark(row+3,col)&&qrCode.isDark(row+4,col)&&!qrCode.isDark(row+5,col)&&qrCode.isDark(row+6,col)){lostPoint+=40;}}}
+	var darkCount=0;for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount;row++){if(qrCode.isDark(row,col)){darkCount++;}}}
+	var ratio=Math.abs(100*darkCount/moduleCount/moduleCount-50)/5;lostPoint+=ratio*10;return lostPoint;}};var QRMath={glog:function(n){if(n<1){throw new Error("glog("+n+")");}
+	return QRMath.LOG_TABLE[n];},gexp:function(n){while(n<0){n+=255;}
+	while(n>=256){n-=255;}
+	return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<<i;}
+	for(var i=8;i<256;i++){QRMath.EXP_TABLE[i]=QRMath.EXP_TABLE[i-4]^QRMath.EXP_TABLE[i-5]^QRMath.EXP_TABLE[i-6]^QRMath.EXP_TABLE[i-8];}
+	for(var i=0;i<255;i++){QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]]=i;}
+	function QRPolynomial(num,shift){if(num.length==undefined){throw new Error(num.length+"/"+shift);}
+	var offset=0;while(offset<num.length&&num[offset]==0){offset++;}
+	this.num=new Array(num.length-offset+shift);for(var i=0;i<num.length-offset;i++){this.num[i]=num[i+offset];}}
+	QRPolynomial.prototype={get:function(index){return this.num[index];},getLength:function(){return this.num.length;},multiply:function(e){var num=new Array(this.getLength()+e.getLength()-1);for(var i=0;i<this.getLength();i++){for(var j=0;j<e.getLength();j++){num[i+j]^=QRMath.gexp(QRMath.glog(this.get(i))+QRMath.glog(e.get(j)));}}
+	return new QRPolynomial(num,0);},mod:function(e){if(this.getLength()-e.getLength()<0){return this;}
+	var ratio=QRMath.glog(this.get(0))-QRMath.glog(e.get(0));var num=new Array(this.getLength());for(var i=0;i<this.getLength();i++){num[i]=this.get(i);}
+	for(var i=0;i<e.getLength();i++){num[i]^=QRMath.gexp(QRMath.glog(e.get(i))+ratio);}
+	return new QRPolynomial(num,0).mod(e);}};function QRRSBlock(totalCount,dataCount){this.totalCount=totalCount;this.dataCount=dataCount;}
+	QRRSBlock.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];QRRSBlock.getRSBlocks=function(typeNumber,errorCorrectLevel){var rsBlock=QRRSBlock.getRsBlockTable(typeNumber,errorCorrectLevel);if(rsBlock==undefined){throw new Error("bad rs block @ typeNumber:"+typeNumber+"/errorCorrectLevel:"+errorCorrectLevel);}
+	var length=rsBlock.length/3;var list=[];for(var i=0;i<length;i++){var count=rsBlock[i*3+0];var totalCount=rsBlock[i*3+1];var dataCount=rsBlock[i*3+2];for(var j=0;j<count;j++){list.push(new QRRSBlock(totalCount,dataCount));}}
+	return list;};QRRSBlock.getRsBlockTable=function(typeNumber,errorCorrectLevel){switch(errorCorrectLevel){case QRErrorCorrectLevel.L:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+0];case QRErrorCorrectLevel.M:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+1];case QRErrorCorrectLevel.Q:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+2];case QRErrorCorrectLevel.H:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+3];default:return undefined;}};function QRBitBuffer(){this.buffer=[];this.length=0;}
+	QRBitBuffer.prototype={get:function(index){var bufIndex=Math.floor(index/8);return((this.buffer[bufIndex]>>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i<length;i++){this.putBit(((num>>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);}
+	if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));}
+	this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]];
+	
+	function _isSupportCanvas() {
+		return typeof CanvasRenderingContext2D != "undefined";
+	}
+	
+	// android 2.x doesn't support Data-URI spec
+	function _getAndroid() {
+		var android = false;
+		var sAgent = navigator.userAgent;
+		
+		if (/android/i.test(sAgent)) { // android
+			android = true;
+			var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i);
+			
+			if (aMat && aMat[1]) {
+				android = parseFloat(aMat[1]);
+			}
+		}
+		
+		return android;
+	}
+	
+	var svgDrawer = (function() {
+
+		var Drawing = function (el, htOption) {
+			this._el = el;
+			this._htOption = htOption;
+		};
+
+		Drawing.prototype.draw = function (oQRCode) {
+			var _htOption = this._htOption;
+			var _el = this._el;
+			var nCount = oQRCode.getModuleCount();
+			var nWidth = Math.floor(_htOption.width / nCount);
+			var nHeight = Math.floor(_htOption.height / nCount);
+
+			this.clear();
+
+			function makeSVG(tag, attrs) {
+				var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
+				for (var k in attrs)
+					if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]);
+				return el;
+			}
+
+			var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight});
+			svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
+			_el.appendChild(svg);
+
+			svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"}));
+			svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"}));
+
+			for (var row = 0; row < nCount; row++) {
+				for (var col = 0; col < nCount; col++) {
+					if (oQRCode.isDark(row, col)) {
+						var child = makeSVG("use", {"x": String(col), "y": String(row)});
+						child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template")
+						svg.appendChild(child);
+					}
+				}
+			}
+		};
+		Drawing.prototype.clear = function () {
+			while (this._el.hasChildNodes())
+				this._el.removeChild(this._el.lastChild);
+		};
+		return Drawing;
+	})();
+
+	var useSVG = document.documentElement.tagName.toLowerCase() === "svg";
+
+	// Drawing in DOM by using Table tag
+	var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () {
+		var Drawing = function (el, htOption) {
+			this._el = el;
+			this._htOption = htOption;
+		};
+			
+		/**
+		 * Draw the QRCode
+		 * 
+		 * @param {QRCode} oQRCode
+		 */
+		Drawing.prototype.draw = function (oQRCode) {
+            var _htOption = this._htOption;
+            var _el = this._el;
+			var nCount = oQRCode.getModuleCount();
+			var nWidth = Math.floor(_htOption.width / nCount);
+			var nHeight = Math.floor(_htOption.height / nCount);
+			var aHTML = ['<table style="border:0;border-collapse:collapse;">'];
+			
+			for (var row = 0; row < nCount; row++) {
+				aHTML.push('<tr>');
+				
+				for (var col = 0; col < nCount; col++) {
+					aHTML.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' + nWidth + 'px;height:' + nHeight + 'px;background-color:' + (oQRCode.isDark(row, col) ? _htOption.colorDark : _htOption.colorLight) + ';"></td>');
+				}
+				
+				aHTML.push('</tr>');
+			}
+			
+			aHTML.push('</table>');
+			_el.innerHTML = aHTML.join('');
+			
+			// Fix the margin values as real size.
+			var elTable = _el.childNodes[0];
+			var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2;
+			var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2;
+			
+			if (nLeftMarginTable > 0 && nTopMarginTable > 0) {
+				elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px";	
+			}
+		};
+		
+		/**
+		 * Clear the QRCode
+		 */
+		Drawing.prototype.clear = function () {
+			this._el.innerHTML = '';
+		};
+		
+		return Drawing;
+	})() : (function () { // Drawing in Canvas
+		function _onMakeImage() {
+			this._elImage.src = this._elCanvas.toDataURL("image/png");
+			this._elImage.style.display = "block";
+			this._elCanvas.style.display = "none";			
+		}
+		
+		// Android 2.1 bug workaround
+		// http://code.google.com/p/android/issues/detail?id=5141
+		if (this._android && this._android <= 2.1) {
+	    	var factor = 1 / window.devicePixelRatio;
+	        var drawImage = CanvasRenderingContext2D.prototype.drawImage; 
+	    	CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) {
+	    		if (("nodeName" in image) && /img/i.test(image.nodeName)) {
+		        	for (var i = arguments.length - 1; i >= 1; i--) {
+		            	arguments[i] = arguments[i] * factor;
+		        	}
+	    		} else if (typeof dw == "undefined") {
+	    			arguments[1] *= factor;
+	    			arguments[2] *= factor;
+	    			arguments[3] *= factor;
+	    			arguments[4] *= factor;
+	    		}
+	    		
+	        	drawImage.apply(this, arguments); 
+	    	};
+		}
+		
+		/**
+		 * Check whether the user's browser supports Data URI or not
+		 * 
+		 * @private
+		 * @param {Function} fSuccess Occurs if it supports Data URI
+		 * @param {Function} fFail Occurs if it doesn't support Data URI
+		 */
+		function _safeSetDataURI(fSuccess, fFail) {
+            var self = this;
+            self._fFail = fFail;
+            self._fSuccess = fSuccess;
+
+            // Check it just once
+            if (self._bSupportDataURI === null) {
+                var el = document.createElement("img");
+                var fOnError = function() {
+                    self._bSupportDataURI = false;
+
+                    if (self._fFail) {
+                        self._fFail.call(self);
+                    }
+                };
+                var fOnSuccess = function() {
+                    self._bSupportDataURI = true;
+
+                    if (self._fSuccess) {
+                        self._fSuccess.call(self);
+                    }
+                };
+
+                el.onabort = fOnError;
+                el.onerror = fOnError;
+                el.onload = fOnSuccess;
+                el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data.
+                return;
+            } else if (self._bSupportDataURI === true && self._fSuccess) {
+                self._fSuccess.call(self);
+            } else if (self._bSupportDataURI === false && self._fFail) {
+                self._fFail.call(self);
+            }
+		};
+		
+		/**
+		 * Drawing QRCode by using canvas
+		 * 
+		 * @constructor
+		 * @param {HTMLElement} el
+		 * @param {Object} htOption QRCode Options 
+		 */
+		var Drawing = function (el, htOption) {
+    		this._bIsPainted = false;
+    		this._android = _getAndroid();
+		
+			this._htOption = htOption;
+			this._elCanvas = document.createElement("canvas");
+			this._elCanvas.width = htOption.width;
+			this._elCanvas.height = htOption.height;
+			el.appendChild(this._elCanvas);
+			this._el = el;
+			this._oContext = this._elCanvas.getContext("2d");
+			this._bIsPainted = false;
+			this._elImage = document.createElement("img");
+			this._elImage.alt = "Scan me!";
+			this._elImage.style.display = "none";
+			this._el.appendChild(this._elImage);
+			this._bSupportDataURI = null;
+		};
+			
+		/**
+		 * Draw the QRCode
+		 * 
+		 * @param {QRCode} oQRCode 
+		 */
+		Drawing.prototype.draw = function (oQRCode) {
+            var _elImage = this._elImage;
+            var _oContext = this._oContext;
+            var _htOption = this._htOption;
+            
+			var nCount = oQRCode.getModuleCount();
+			var nWidth = _htOption.width / nCount;
+			var nHeight = _htOption.height / nCount;
+			var nRoundedWidth = Math.round(nWidth);
+			var nRoundedHeight = Math.round(nHeight);
+
+			_elImage.style.display = "none";
+			this.clear();
+			
+			for (var row = 0; row < nCount; row++) {
+				for (var col = 0; col < nCount; col++) {
+					var bIsDark = oQRCode.isDark(row, col);
+					var nLeft = col * nWidth;
+					var nTop = row * nHeight;
+					_oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
+					_oContext.lineWidth = 1;
+					_oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;					
+					_oContext.fillRect(nLeft, nTop, nWidth, nHeight);
+					
+					// 안티 앨리어싱 방지 처리
+					_oContext.strokeRect(
+						Math.floor(nLeft) + 0.5,
+						Math.floor(nTop) + 0.5,
+						nRoundedWidth,
+						nRoundedHeight
+					);
+					
+					_oContext.strokeRect(
+						Math.ceil(nLeft) - 0.5,
+						Math.ceil(nTop) - 0.5,
+						nRoundedWidth,
+						nRoundedHeight
+					);
+				}
+			}
+			
+			this._bIsPainted = true;
+		};
+			
+		/**
+		 * Make the image from Canvas if the browser supports Data URI.
+		 */
+		Drawing.prototype.makeImage = function () {
+			if (this._bIsPainted) {
+				_safeSetDataURI.call(this, _onMakeImage);
+			}
+		};
+			
+		/**
+		 * Return whether the QRCode is painted or not
+		 * 
+		 * @return {Boolean}
+		 */
+		Drawing.prototype.isPainted = function () {
+			return this._bIsPainted;
+		};
+		
+		/**
+		 * Clear the QRCode
+		 */
+		Drawing.prototype.clear = function () {
+			this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height);
+			this._bIsPainted = false;
+		};
+		
+		/**
+		 * @private
+		 * @param {Number} nNumber
+		 */
+		Drawing.prototype.round = function (nNumber) {
+			if (!nNumber) {
+				return nNumber;
+			}
+			
+			return Math.floor(nNumber * 1000) / 1000;
+		};
+		
+		return Drawing;
+	})();
+	
+	/**
+	 * Get the type by string length
+	 * 
+	 * @private
+	 * @param {String} sText
+	 * @param {Number} nCorrectLevel
+	 * @return {Number} type
+	 */
+	function _getTypeNumber(sText, nCorrectLevel) {			
+		var nType = 1;
+		var length = _getUTF8Length(sText);
+		
+		for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
+			var nLimit = 0;
+			
+			switch (nCorrectLevel) {
+				case QRErrorCorrectLevel.L :
+					nLimit = QRCodeLimitLength[i][0];
+					break;
+				case QRErrorCorrectLevel.M :
+					nLimit = QRCodeLimitLength[i][1];
+					break;
+				case QRErrorCorrectLevel.Q :
+					nLimit = QRCodeLimitLength[i][2];
+					break;
+				case QRErrorCorrectLevel.H :
+					nLimit = QRCodeLimitLength[i][3];
+					break;
+			}
+			
+			if (length <= nLimit) {
+				break;
+			} else {
+				nType++;
+			}
+		}
+		
+		if (nType > QRCodeLimitLength.length) {
+			throw new Error("Too long data");
+		}
+		
+		return nType;
+	}
+
+	function _getUTF8Length(sText) {
+		var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a');
+		return replacedText.length + (replacedText.length != sText ? 3 : 0);
+	}
+	
+	/**
+	 * @class QRCode
+	 * @constructor
+	 * @example 
+	 * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie");
+	 *
+	 * @example
+	 * var oQRCode = new QRCode("test", {
+	 *    text : "http://naver.com",
+	 *    width : 128,
+	 *    height : 128
+	 * });
+	 * 
+	 * oQRCode.clear(); // Clear the QRCode.
+	 * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode.
+	 *
+	 * @param {HTMLElement|String} el target element or 'id' attribute of element.
+	 * @param {Object|String} vOption
+	 * @param {String} vOption.text QRCode link data
+	 * @param {Number} [vOption.width=256]
+	 * @param {Number} [vOption.height=256]
+	 * @param {String} [vOption.colorDark="#000000"]
+	 * @param {String} [vOption.colorLight="#ffffff"]
+	 * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H] 
+	 */
+	QRCode = function (el, vOption) {
+		this._htOption = {
+			width : 256, 
+			height : 256,
+			typeNumber : 4,
+			colorDark : "#000000",
+			colorLight : "#ffffff",
+			correctLevel : QRErrorCorrectLevel.H
+		};
+		
+		if (typeof vOption === 'string') {
+			vOption	= {
+				text : vOption
+			};
+		}
+		
+		// Overwrites options
+		if (vOption) {
+			for (var i in vOption) {
+				this._htOption[i] = vOption[i];
+			}
+		}
+		
+		if (typeof el == "string") {
+			el = document.getElementById(el);
+		}
+
+		if (this._htOption.useSVG) {
+			Drawing = svgDrawer;
+		}
+		
+		this._android = _getAndroid();
+		this._el = el;
+		this._oQRCode = null;
+		this._oDrawing = new Drawing(this._el, this._htOption);
+		
+		if (this._htOption.text) {
+			this.makeCode(this._htOption.text);	
+		}
+	};
+	
+	/**
+	 * Make the QRCode
+	 * 
+	 * @param {String} sText link data
+	 */
+	QRCode.prototype.makeCode = function (sText) {
+		this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel);
+		this._oQRCode.addData(sText);
+		this._oQRCode.make();
+		this._el.title = sText;
+		this._oDrawing.draw(this._oQRCode);			
+		this.makeImage();
+	};
+	
+	/**
+	 * Make the Image from Canvas element
+	 * - It occurs automatically
+	 * - Android below 3 doesn't support Data-URI spec.
+	 * 
+	 * @private
+	 */
+	QRCode.prototype.makeImage = function () {
+		if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) {
+			this._oDrawing.makeImage();
+		}
+	};
+	
+	/**
+	 * Clear the QRCode
+	 */
+	QRCode.prototype.clear = function () {
+		this._oDrawing.clear();
+	};
+	
+	/**
+	 * @name QRCode.CorrectLevel
+	 */
+	QRCode.CorrectLevel = QRErrorCorrectLevel;
+})();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/register.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,51 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local page_header = Shared.page_header or error()
+local footer = Shared.footer or error()
+local password_input = Shared.password_input or error()
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<meta name="robots" content="noindex,nofollow"/>
+		<style>
+			div[right_of_page] {
+				background-image: url(/images/model.jpg);
+			}
+			div[login] {
+				text-align: center;
+				margin-top: 20px;
+			}
+		</style>
+	</head>
+	<body>
+		<form page onsubmit="ajaxForm('/register.js',this)" action="javascript:">
+<%			page_header() %>
+			<div>
+				<h1>Create your account</h1>
+				<p>Choose your username. You can always change it later.</p>
+				<input type=text required name=username placeholder="Username">
+				<div error=username></div>
+				<input type=email required name=email placeholder="Email">
+				<div error=email></div>
+<%				password_input() %>
+				<button type=submit big>Create account</button>
+				<div login>Already registered? <a href="/login.html">Log in</a></div>
+			</div>
+<%			footer() %>
+		</form>
+		<div right_of_page></div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/register.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,97 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local String = require "luan:String.luan"
+local to_lower = String.lower or error()
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local User = require "site:/lib/User.luan"
+local name_regex = User.name_regex
+local new_code = User.new_code
+local Utils = require "site:/lib/Utils.luan"
+local email_regex = Utils.email_regex
+local base_url = Utils.base_url or error()
+local warn = Utils.warn or error()
+local Shared = require "site:/lib/Shared.luan"
+local js_error = Shared.js_error or error()
+local send_mail_async = Shared.send_mail_async or error()
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "register.js"
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+	local username = Http.request.parameters.username or error()
+	local email = Http.request.parameters.email or error()
+	local password = Http.request.parameters.password or error()
+	email_regex.matches(email) or error("bad email: "..email)
+	if not name_regex.matches(username) then
+		js_error( "username", [[Usernames may only contain letters, numbers, underscores ("_") and hyphens ("-")]] )
+		return
+	end
+	local user
+	local err_fld, err_msg = run_in_transaction( function()
+		user = User.get_by_email(email)
+		if user == nil then
+			if User.get_by_name(username) ~= nil then
+				return "username", "This username is already taken"
+			end
+			user = User.new{ name=username, email=email, password=password }
+		else
+			if to_lower(user.name) ~= to_lower(username) and User.get_by_name(username) ~= nil then
+				return "username", "This username is already taken"
+			end
+			if user.registered ~= nil then
+				return "email", "This email is already in use"
+			end
+			user.name = username
+			user.password = password
+		end
+		user.code = user.code or new_code()
+		user.save()
+	end )
+	if err_fld ~= nil then
+		js_error(err_fld,err_msg)
+		return
+	end
+	logger.info("code = "..user.code)
+	local url = base_url().."/register2.html?user="..user.name.."&code="..user.code
+	send_mail_async {
+		From = "Link My Style <support@linkmy.style>"
+		To = email
+		Subject = "Confirmation Code"
+		["MIME-Version"] = "1.0"
+		["Content-Type"] = "multipart/alternative"
+		body = {
+			{
+				["Content-Type"] = [[text/plain; charset="UTF-8"]]
+				body = `%>
+Thank you for registering.  Please click the link below or use the 6 digit confirmation code to complete the process:
+
+<%=url%>
+
+Confirmation Code: <%=user.code%>
+
+If you did not request this code, please ignore this email.
+<%				`
+			}
+			{
+				["Content-Type"] = [[text/html; charset="UTF-8"]]
+				body = `%>
+Thank you for registering.  Please <a href="<%=html_encode(url)%>">click here</a> or use the 6 digit confirmation code below to complete the process:<br>
+<br>
+Confirmation Code: <b><%=user.code%></b><br>
+<br>
+If you did not request this code, please ignore this email.<br>
+<%				`
+			}
+		}
+	}
+%>
+	clearErrors(context.form);
+	location = '/register2.html?user=<%=username%>';
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/register2.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,61 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local page_header = Shared.page_header or error()
+local footer = Shared.footer or error()
+local User = require "site:/lib/User.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "register2.html"
+
+
+return function()
+	local user_name = Http.request.parameters.user
+	if user_name == nil then
+		Http.response.send_redirect "/register.html"
+		return
+	end
+	local code = Http.request.parameters.code or ""
+	local user = User.get_by_name(user_name) or error()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+			div[right_of_page] {
+				background-image: url(/images/bag.jpg);
+			}
+			p[info] {
+				color: #808080 !important;
+			}
+			[spam] {
+				font-weight: bold;
+				color: #ad1f00;
+			}
+		</style>
+	</head>
+	<body>
+		<form page onsubmit="ajaxForm('/register2.js',this)" action="javascript:">
+<%			page_header() %>
+			<div>
+				<input type=hidden name="user" value="<%=user_name%>">
+				<h1>Enter confirmation code</h1>
+				<p info>To verify your account, enter the six digit code we sent to <%=html_encode(user.email)%>.  <span spam>If you do not see our email, please check your spam folder for the email and report not spam.</span></p>
+				<input type=text required name=code value="<%=code%>" placeholder="Enter 6-digit code" autofocus>
+				<div error=code></div>
+				<button type=submit big>Continue</button>
+			</div>
+<%			footer() %>
+		</form>
+		<div right_of_page></div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/register2.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,46 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Time = require "luan:Time.luan"
+local time_now = Time.now or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local User = require "site:/lib/User.luan"
+local Shared = require "site:/lib/Shared.luan"
+local js_error = Shared.js_error or error()
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "register2.js"
+
+
+return function()
+	local user_name = Http.request.parameters.user or error()
+	local user = User.get_by_name(user_name) or error(user_name)
+	local code = Http.request.parameters.code or error()
+	local err_fld, err_msg = run_in_transaction( function()
+		user = user.reload()
+		if user.registered ~= nil then
+			return "code", "You have already registered"
+		end
+		if user.code ~= code then
+			return "code", "Incorrect code"
+		end
+		user.code = nil
+		user.registered = time_now()
+		user.source = Http.request.cookies.source
+		Http.response.remove_cookie("source")
+		Http.response.remove_cookie("seller")
+		user.save()
+	end )
+	Io.stdout = Http.response.text_writer()
+	if err_fld ~= nil then
+		js_error(err_fld,err_msg)
+		logger.warn(err_msg)
+		return
+	end
+	user.login()
+%>
+	clearErrors(context.form);
+	location = '/register3.html';
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/register3.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,44 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local page_header = Shared.page_header or error()
+local footer = Shared.footer or error()
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+			div[right_of_page] {
+				background-image: url(/images/picture.jpg);
+			}
+		</style>
+		<script>
+			if( hasCookies ) {
+				fbTrack( 'track', 'CompleteRegistration' );
+				// fbTrack( 'track', 'Purchase', {value:1,currency:'USD'} );
+			}
+		</script>
+	</head>
+	<body>
+		<div page>
+<%			page_header() %>
+			<div>
+				<h1>Thanks for signing up</h1>
+				<a button big href="/account.html">Continue to edit My Account</a>
+			</div>
+<%			footer() %>
+		</form>
+		<div right_of_page></div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/report.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,82 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local stringify = Luan.stringify or error()
+local String = require "luan:String.luan"
+local to_number = String.to_number or error()
+local Number = require "luan:Number.luan"
+local long = Number.long or error()
+local Table = require "luan:Table.luan"
+local copy = Table.copy or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Reporting = require "site:/lib/Reporting.luan"
+local db = Reporting.db or error()
+local Utils = require "site:/lib/Utils.luan"
+local to_list = Utils.to_list or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "report.js"
+
+
+-- modified from Lucene.luan
+local function get_document(query)
+	local doc
+	local function fn(_,doc_fn,_)
+		doc = doc_fn()
+	end
+	local total_hits = db.advanced_search(query,fn,1)
+	if total_hits > 1 then
+		logger.error("found "..total_hits.." documents for query: "..stringify(query))
+	end
+	return doc
+end
+
+return function()
+	local types = Http.request.parameters.type or error()
+	types = to_list(types)
+	local owner = Http.request.parameters.owner or error()
+	local today = Http.request.parameters.today or error()
+	today = to_number(today) or error(today)
+	today = long(today)
+	db.run_in_transaction( function()
+		for _, type in ipairs(types) do
+			local value = Http.request.parameters[type]
+			local before = Http.request.parameters[type.."_before"]
+			before = before and long(to_number(before))
+			--logger.info(type.." ~ "..owner.." ~ "..value)
+			local keys = {
+				type = type
+				owner = owner
+				value = value
+				day = today
+			}
+			local found = get_document(keys)
+			if found == nil then
+				local doc = copy(keys)
+				doc.count = 1
+				db.save(doc)
+				--logger.info(1)
+			else
+				found.count = found.count + 1
+				db.save(found)
+				--logger.info(found.count)
+			end
+			if before ~= nil then
+				keys.day = before
+				found = get_document(keys)
+				if found == nil then
+					logger.warn("before not found: "..stringify(keys))
+				else
+					local count = found.count
+					if count <= 0 then
+						logger.warn("before count is "..count.." for: "..stringify(keys))
+					else
+						found.count = count - 1
+						db.save(found)
+						--logger.info("before")
+					end
+				end
+			end
+		end_for
+	end )
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/reporting.js	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,106 @@
+'use strict';
+
+(function() {
+
+if(!hasCookies) return;
+
+let hour = 60*60*1000;
+let day = 24*hour;
+let offsetFromGMT = 7*hour;
+let today = Math.floor((Date.now() - offsetFromGMT) / day) * day + offsetFromGMT;
+// today -= 28*day;
+let expired = today - 30*day;
+
+let owner = window.Owner;
+let page = window.Page;
+let changed = false;
+let values = {};
+
+let data = localStorage && localStorage.reporting;
+data = data ? JSON.parse(data) : {};
+for( let key in data ) {
+	//console.log(key);
+	if( data[key] < expired ) {
+		//console.log(`delete ${key} ${data[key]} ${expired}`);
+		delete data[key];
+		changed = true;
+	}
+}
+
+function recordChange() {
+	if( !changed || !localStorage )
+		return;
+	localStorage.reporting = JSON.stringify(data);
+	if( Object.keys(values).length > 0 ) {
+		let url = `/report.js?owner=${encodeURIComponent(owner)}&today=${today}`;
+		for( let type in values ) {
+			url += `&type=${type}`;
+			let info = values[type];
+			let value = info.value;
+			let before = info.before;
+			if( value )
+				url += `&${type}=${encodeURIComponent(value)}`;
+			if( before )
+				url += `&${type}_before=${before}`;
+		}
+		ajax(url);
+		values = {};
+	}
+	changed = false;
+}
+
+function reportDay(type) {
+	let key = JSON.stringify({ type:type, owner:owner });
+	let val = data[key];
+	if( !val || val < today ) {
+		values[type] = {};
+		data[key] = today
+		changed = true;
+	}
+}
+
+function reportMonth(type,value) {
+	let key = JSON.stringify({ type:type, owner:owner, value:value });
+	let val = data[key];
+	if( !val || val < today ) {
+		let info = {};
+		if( value )
+			info.value = value;
+		if( val )
+			info.before = val;
+		values[type] = info;
+		data[key] = today;
+		changed = true;
+	}
+}
+
+reportDay('visit');
+reportMonth('monthly_visit');
+reportMonth('page_view',page);
+
+let referringDomain = document.referrer;
+referringDomain = referringDomain ? new URL(referringDomain).hostname : 'direct';
+if( referringDomain && referringDomain !== location.hostname ) {
+	reportMonth('referrer',referringDomain);
+}
+
+recordChange();
+
+function onclick(event) {
+	//console.log(event);
+	//console.log(event.target.textContent);
+	reportDay('click');
+	reportMonth('monthly_click');
+	let link = page + ' - ' + event.target.textContent;
+	reportMonth('link_click',link);
+	recordChange();
+	//return false;
+}
+let links = document.querySelectorAll('a[link]');
+for( let i=0; i<links.length; i++ ) {
+	let link = links[i];
+	link.onclick = onclick;
+	//link.addEventListener('click',onclick);
+}
+
+})();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/save_account.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,76 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local String = require "luan:String.luan"
+local to_lower = String.lower or error()
+local trim = String.trim or error()
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local js_error = Shared.js_error or error()
+local User = require "site:/lib/User.luan"
+local name_regex = User.name_regex
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+	local title = Http.request.parameters.title or error()
+	local bio = Http.request.parameters.bio or error()
+	local username = Http.request.parameters.username or error()
+	local password = Http.request.parameters.password or error()
+	local pic_uuid = Http.request.parameters.pic_uuid or error()
+	local pic_filename = Http.request.parameters.pic_filename or error()
+
+	if not name_regex.matches(username) then
+		js_error("username",[[Usernames may only contain letters, numbers, underscores ("_") and hyphens ("-")]])
+		return
+	end
+	local pic_changed = false
+	local user = User.current() or error()
+	local err_fld, err_msg = run_in_transaction( function()
+		user = user.reload()
+		if to_lower(user.name) ~= to_lower(username) and User.get_by_name(username) ~= nil then
+			return "username", "This username is already taken"
+		end
+		if title=="" or title==username or title==user.name then
+			title = nil
+		end
+		user.title = title
+		bio = trim(bio)
+		if bio == "" then
+			bio = nil
+		end
+		user.bio = bio
+		user.name = username
+		user.password = password
+		if pic_uuid == "" then
+			-- no change
+		elseif pic_uuid == "remove" then
+			user.pic_uuid = nil
+			user.pic_filename = nil
+			pic_changed = true
+		else
+			user.pic_uuid = pic_uuid
+			user.pic_filename = pic_filename
+			pic_changed = true
+		end
+		user.save()
+		user.login()
+	end )
+	if err_fld ~= nil then
+		js_error(err_fld,err_msg)
+		return
+	end
+	js_error( "success", "Your information has been updated successfully." )
+	if pic_changed then
+		local url = user.get_pic_url() or "/images/user.png"
+%>
+		let img = document.querySelector('div[header] > span[right] img');
+		if(img)
+			img.src = <%=json_string(url)%>;
+<%
+	end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/save_email.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,52 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local String = require "luan:String.luan"
+local to_lower = String.lower or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local js_error = Shared.js_error or error()
+local send_mail_async = Shared.send_mail_async or error()
+local Utils = require "site:/lib/Utils.luan"
+local email_regex = Utils.email_regex
+local User = require "site:/lib/User.luan"
+local new_code = User.new_code
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+
+
+return function()
+	local user = User.current() or error()
+	local email = Http.request.parameters.email or error()
+	email_regex.matches(email) or error("bad email: "..email)
+	local err_fld, err_msg = run_in_transaction( function()
+		user = user.reload()
+		if to_lower(user.email) == to_lower(email) then
+			return "email", "Email unchanged"
+		end
+		if User.get_by_name(email) ~= nil then
+			return "email", "This email is already taken"
+		end
+		user.new_email = email
+		user.code = new_code()
+		user.save()
+	end )
+	Io.stdout = Http.response.text_writer()
+	if err_fld ~= nil then
+		js_error(err_fld,err_msg)
+		return
+	end
+	send_mail_async {
+		From = "Link My Style <support@linkmy.style>"
+		To = email
+		Subject = "Confirmation code"
+		body = `%>
+Here is your 6 digit confirmation code:
+<%=user.code%>
+<%`
+	}
+%>
+	clearErrors(context.form);
+	location = '/change_email.html';
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/save_icons.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,64 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local pairs = Luan.pairs or error()
+local String = require "luan:String.luan"
+local trim = String.trim or error()
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local show_user_icons = Shared.show_user_icons or error()
+local User = require "site:/lib/User.luan"
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+local Icon = require "site:/lib/Icon.luan"
+local icon_names = Icon.icon_names or error()
+local get_user_icons = Icon.get_user_icons or error()
+local icon_from_doc = Icon.from_doc or error()
+
+
+return function()
+	local user = User.current() or error()
+	local user_id = user.id
+	run_in_transaction( function()
+		local order
+		do
+			local icons = get_user_icons(user_id)
+			order = #icons > 0 and icons[#icons].order or 0
+		end
+		for name, info in pairs(icon_names) do
+			local url = Http.request.parameters[name] or error()
+			url = trim(url)
+			if url == '' then
+				Db.delete("+icon_user_id:"..user_id.." +icon_name:"..name)
+			else
+				if info.type == "email" then
+					url = "mailto:"..url
+				end
+				local doc = Db.get_document("+icon_user_id:"..user_id.." +icon_name:"..name)
+				if doc == nil then
+					order = order + 1
+					local icon = Icon.new{
+						name = name
+						url = url
+						user_id = user_id
+						order = order
+					}
+					icon.save()
+				else
+					local icon = icon_from_doc(doc)
+					icon.url = url
+					icon.save()
+				end
+			end
+		end
+	end )
+	local html = ` show_user_icons(user) `
+	Io.stdout = Http.response.text_writer()
+%>
+	document.querySelector('div[icons]').innerHTML = <%= json_string(html) %>;
+	document.querySelector('h2[icons]').scrollIntoViewIfNeeded(false);
+	dragInit();
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/save_link.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,34 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Link = require "site:/lib/Link.luan"
+local Shared = require "site:/lib/Shared.luan"
+local show_editable_link = Shared.show_editable_link or error()
+local User = require "site:/lib/User.luan"
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+
+
+return function()
+	local user = User.current() or error()
+	local link_id = Http.request.parameters.link or error()
+	local url = Http.request.parameters.url or error()
+	local title = Http.request.parameters.title or error()
+	local link = Link.get_by_id(link_id)
+	link.user_id == user.id or error()
+	run_in_transaction( function()
+		link = link.reload()
+		link.url = url
+		link.title = title
+		link.save()
+	end )
+	local html = ` show_editable_link(link) `
+	Io.stdout = Http.response.text_writer()
+%>
+	document.querySelector('div[link="<%=link_id%>"]').outerHTML = <%= json_string(html) %>;
+	dragInit();
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/save_mp.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,29 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local String = require "luan:String.luan"
+local trim = String.trim or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local js_error = Shared.js_error or error()
+local Pic = require "site:/lib/Pic.luan"
+local User = require "site:/lib/User.luan"
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+
+
+return function()
+	local user = User.current() or error()
+	local mp_id = Http.request.parameters.mp_id or error()
+	mp_id = trim(mp_id)
+	if mp_id == "" then
+		mp_id = nil
+	end
+	run_in_transaction( function()
+		user = user.reload()
+		user.mp_id = mp_id
+		user.save()
+	end )
+	Io.stdout = Http.response.text_writer()
+	js_error( "success", "Updated" )
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/save_pic_title.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,59 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local pairs = Luan.pairs or error()
+local String = require "luan:String.luan"
+local trim = String.trim or error()
+local regex = String.regex or error()
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local js_error = Shared.js_error or error()
+local Pic = require "site:/lib/Pic.luan"
+local User = require "site:/lib/User.luan"
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "save_pic_title.js"
+
+
+local hashtags_regex = regex[[^(#?\w+\s*)*$]]
+local hashtag_regex = regex[[\w+]]
+
+return function()
+	Io.stdout = Http.response.text_writer()
+	local user = User.current() or error()
+	local pic_id = Http.request.parameters.pic or error()
+	local title = Http.request.parameters.title or error()
+	title = trim(title)
+	local hashtags = Http.request.parameters.hashtags or error()
+	hashtags = trim(hashtags)
+	local is_hidden = Http.request.parameters.visible == nil
+	if not hashtags_regex.matches(hashtags) then
+		js_error("hashtags",[[Hashtags may only contain letters, numbers, underscores ("_"), and spaces to separate them]])
+		return
+	end
+	local set = {}
+	for hashtag in hashtag_regex.gmatch(hashtags) do
+		set[hashtag] = true
+	end
+	hashtags = {}
+	for hashtag in pairs(set) do
+		hashtags[#hashtags+1] = hashtag
+	end
+	local pic
+	run_in_transaction( function()
+		pic = Pic.get_by_id(pic_id)
+		pic.user_id == user.id or error()
+		pic.title = title
+		pic.is_hidden = is_hidden
+		pic.hashtags = hashtags
+		pic.save()
+	end )
+	js_error( "success", "Saved" )
+%>
+	document.querySelector('div[pic] img').title = <%=json_string(title)%>;
+	document.querySelector('div[hashtags]').innerHTML = <%=json_string(pic.hashtags_html("pics.html"))%>;
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/site.css	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,223 @@
+* {
+	box-sizing: border-box;
+}
+
+body {
+	font-family: Bitter;
+	margin: 0;
+}
+body[colored] {
+	background-color: #C0D1D6;
+}
+
+a[link] {
+	display: block;
+	border-radius: 12px / 50%;
+	color: white;
+	background-color: #325762;
+	text-align: center;
+	padding: 12px;
+	text-decoration: none;
+	margin-bottom: 10px;
+}
+a[link]:hover {
+	background-color: #243F47;
+}
+
+div[links] {
+	max-width: 600px;
+	margin-left: auto;
+	margin-right: auto;
+}
+@media (max-width: 700px) {
+	div[links] {
+		max-width: 90%;
+	}
+}
+
+div[saved] {
+	background-color: white;
+	padding-top: 10px;
+	padding-bottom: 10px;
+	color: green;
+	text-align: center;
+	position: relative;
+}
+div[saved] p {
+	margin-top: 8px;
+	margin-bottom: 8px;
+}
+div[saved] a {
+	text-decoration: none;
+}
+div[saved] a[close] {
+	position: absolute;
+	top: 10px;
+	right: 10px;
+	display: block;
+}
+div[saved] img {
+	display: block;
+	height: 26px;
+	opacity: 0.7;
+}
+
+div[pics] {
+	display: flex;
+	gap: 3%;
+	flex-wrap: wrap;
+}
+div[pics] span {
+	width: 22.75%;
+	aspect-ratio: 1;
+	margin-bottom: 3%;
+}
+@media (max-width: 800px) {
+	div[pics] span {
+		width: 48.5%;
+	}
+}
+div[pics] a img {
+	display: block;
+	width: 100%;
+	border-radius: 4px;
+}
+
+div[back] a {
+	display: inline-block;
+	margin-left: 20px;
+	margin-top: 10px;
+}
+div[back] img {
+	width: 30px;
+}
+
+html[pic] div[hashtags] {
+	margin-top: 8px;
+}
+div[hashtags] a {
+	text-decoration: none;
+	color: black;
+}
+
+
+html[main] body {
+	text-align: center;
+}
+html[main] div[home] {
+	padding-top: 8px;
+	margin-bottom: 60px;
+}
+html[main] div[home] a {
+	text-decoration: none;
+	color: black;
+}
+html[main] div[home] a:hover {
+	text-decoration: underline;
+}
+html[main] div[home] span[lms] {
+	font-family: Fraunces;
+	font-size: 18px;
+	font-weight: bold;
+}
+html[main] div[home] span[small] {
+	font-size: 11px;
+}
+html[main] img[user] {
+	display: block;
+	width: 100px;
+	height: 100px;
+	object-fit: cover;
+	border-radius: 50%;
+	margin-left: auto;
+	margin-right: auto;
+}
+html[main] h1 {
+	font-size: 24px;
+	font-weight: normal;
+}
+html[main] div[bio] {
+	white-space: pre-wrap;
+	margin-bottom: 20px;
+	max-width: 600px;
+	margin-left: auto;
+	margin-right: auto;
+}
+html[main] div[icons] {
+	display: flex;
+	justify-content: center;
+	flex-wrap: wrap;
+	gap: 10px;
+	margin-bottom: 20px;
+}
+html[main] div[icons] img {
+	display: block;
+	height: 40px;
+}
+html[main] div[hashtags] {
+	margin-top: 40px;
+}
+
+html[main] div[pics] {
+	width: 90%;
+	margin-top: 40px;
+	margin-left: auto;
+	margin-right: auto;
+}
+
+html[pic] img[pic] {
+	width: 100%;
+	display: block;
+}
+
+html[pic] div[body] {
+	margin-top: 40px;
+	margin-bottom: 40px;
+}
+@media (min-width: 888px) {
+	html[pic] div[body] {
+		display: flex;
+		align-items: flex-start;
+	}
+	html[pic] div[left] {
+		width: 45%;
+		margin-left: 5%;
+	}
+	html[pic] div[outer_links] {
+		width: 55%;
+		margin-left: 5%;
+		margin-right: 5%;
+	}
+}
+@media (max-width: 887px) {
+	html[pic] div[left] {
+		width: 90%;
+		margin-left: auto;
+		margin-right: auto;
+		margin-bottom: 40px;
+	}
+}
+
+span[select] {
+	display: inline-block;
+	color: white;
+}
+span[select] select {
+	background-color: #325762;
+	padding: 12px;
+	padding-right: calc(12px + 1em);
+	border-radius: 12px / 50%;
+	border: none;
+	appearance: none;
+	cursor: pointer;
+	color: inherit;
+	outline: none;
+}
+span[select] select:hover {
+	background-color: #243F47;
+}
+span[select]:after {
+	content: "⋁";
+	margin-left: -1.5em;
+	pointer-events: none;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/site.js	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,250 @@
+'use strict';
+
+function ajax(url,postData,context) {
+	let request = new XMLHttpRequest();
+	let method = postData ? 'POST' : 'GET';
+	request.open( method, url );
+	if( postData )
+		request.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
+	request.onload = function() {
+		if( request.status !== 200 ) {
+			let err = 'ajax failed: ' + request.status;
+			if( request.responseText ) {
+				err += '\n' + request.responseText.trim();
+				document.write('<pre>'+request.responseText+'</pre>');
+			}
+			err += '\nurl = ' + url;
+			err += '\npage = ' + window.location;
+			ajax( '/error_log.js', 'err='+encodeURIComponent(err) );
+			return;
+		}
+		try {
+			eval( request.responseText );
+		} catch(e) {
+			console.log( request.responseText );
+			window.err = '\najax-url = ' + url;
+			throw e;
+		}
+	};
+	request.send(postData);
+}
+
+window.onerror = function(msg, url, line, col, error) {
+	if( !url )
+		return;
+	let err = msg;
+	err += '\nurl = ' + url;
+	if( url != window.location )
+		err += '\npage = ' + window.location;
+	err += '\nline = '+line;
+	if( col )
+		err += '\ncolumn = ' + col;
+	if( error ) {
+		if( error.stack )
+			err += '\nstack = ' + error.stack;
+		if( error.cause )
+			err += '\ncause= ' + error.cause;
+		if( error.fileName )
+			err += '\nfileName= ' + error.fileName;
+	}
+	if( window.err ) {
+		err += window.err;
+		window.err = null;
+	}
+	ajax( '/error_log.js', 'err='+encodeURIComponent(err) );
+};
+
+window.onunhandledrejection = function(event) {
+	//console.log(event);
+	let reason = event.reason;
+	let err = reason && reason.message || reason || JSON.stringify(event);
+	if( reason && reason.stack )
+		err += '\nstack = ' + reason.stack;
+	ajax( '/error_log.js', 'err='+encodeURIComponent(err) );
+};
+
+let isProduction = location.hostname==='linkmy.style';
+
+let cookies;
+function setCookies() {
+	cookies = {};
+	if( document.cookie !== '' ) {
+		let a = document.cookie.split('; ');
+		for( let i=0; i<a.length; i++ ) {
+			let s = a[i];
+			let j = s.indexOf('=');
+			if( j !== -1 ) {
+				let name = s.substring(0,j);
+				let value = s.substring(j+1);
+				cookies[name] = decodeURIComponent(value);
+			}
+		}
+	}
+}
+setCookies();
+
+document.cookie = 'testcookie=x; Max-Age=1;';
+document.cookie = '';
+let hasCookies = document.cookie.includes('testcookie');
+
+// Mixpanel
+const MIXPANEL_CUSTOM_LIB_URL = "https://mp.linkmy.style/lib.min.js";
+
+(function (f, b) { if (!b.__SV) { var e, g, i, h; window.mixpanel = b; b._i = []; b.init = function (e, f, c) { function g(a, d) { var b = d.split("."); 2 == b.length && ((a = a[b[0]]), (d = b[1])); a[d] = function () { a.push([d].concat(Array.prototype.slice.call(arguments, 0))); }; } var a = b; "undefined" !== typeof c ? (a = b[c] = []) : (c = "mixpanel"); a.people = a.people || []; a.toString = function (a) { var d = "mixpanel"; "mixpanel" !== c && (d += "." + c); a || (d += " (stub)"); return d; }; a.people.toString = function () { return a.toString(1) + ".people (stub)"; }; i = "disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking start_batch_senders people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split( " "); for (h = 0; h < i.length; h++) g(a, i[h]); var j = "set set_once union unset remove delete".split(" "); a.get_group = function () { function b(c) { d[c] = function () { call2_args = arguments; call2 = [c].concat(Array.prototype.slice.call(call2_args, 0)); a.push([e, call2]); }; } for ( var d = {}, e = ["get_group"].concat( Array.prototype.slice.call(arguments, 0)), c = 0; c < j.length; c++) b(j[c]); return d; }; b._i.push([e, f, c]); }; b.__SV = 1.2; e = f.createElement("script"); e.type = "text/javascript"; e.async = !0; e.src = "undefined" !== typeof MIXPANEL_CUSTOM_LIB_URL ? MIXPANEL_CUSTOM_LIB_URL : "file:" === f.location.protocol && "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//) ? "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js" : "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js"; g = f.getElementsByTagName("script")[0]; g.parentNode.insertBefore(e, g); } })(document, window.mixpanel || []);
+
+let mpConfig = {
+	debug: !isProduction,
+	persistence: 'localStorage',
+	api_host: 'https://mp.linkmy.style',
+};
+let mixpanelProps = {};
+{
+	if( isProduction ) {
+		mixpanel.init('404d4c479de9c3070252e692375e82ca',mpConfig,'ours');
+	} else {  // test
+		mixpanel.init('bd2099a22e4118350a46b5b360d8c4da',mpConfig,'ours');
+	}
+	if( !hasCookies || location.pathname.match(/^\/private\//) ) {
+		mixpanel.ours.disable();
+	} else {
+		let props = mixpanelProps;
+		if( navigator.userAgent.includes('Instagram') ) {
+			props.App = 'Instagram';
+		} else if( navigator.userAgent.includes('BytedanceWebview') ) {
+			props.App = 'TikTok';
+		}
+		props.LoggedIn = !!cookies.user;
+		if( typeof(URLSearchParams) === 'function' ) {
+			let searchParams = new URLSearchParams(location.search);
+			if( Symbol.iterator in searchParams ) {
+				for (let p of searchParams) {
+					props[p[0]] = p[1];
+				}
+			}
+		}
+		props.Path = window.Path || location.pathname;
+		if( !window.Owner ) {
+			mixpanel.ours.track( 'Page View', props );
+		} else {
+			props.Owner = window.Owner;
+			props.Page = window.Page;
+			if( isProduction ) {
+				mixpanel.init('2bb632e9309b175ef67b7df382a43aee',mpConfig,'rpt');
+			} else {  // test
+				mixpanel.init('4acad1a74dfe7dba21e30da2642b9f78',mpConfig,'rpt');
+			}
+			if( Math.random() > 0.01 )
+				mixpanel.rpt.disable();
+			mixpanel.rpt.track( 'Page View', props );
+			window.addEventListener( 'load', function() {
+				let links = document.querySelectorAll('a[link]');
+				for( let i=0; i<links.length; i++ ) {
+					let link = links[i];
+					link.addEventListener( 'click', function(event) {
+						props.Link = event.target.textContent;
+						mixpanel.rpt.track( 'Click', props, {send_immediately:true} );
+					} );
+				}
+			} );
+			if( window.OwnerMpId ) {
+				mixpanel.init(window.OwnerMpId,mpConfig,'owner');
+				let props = { Page: window.Page };
+				mixpanel.owner.track( 'Page View', props );
+				window.addEventListener( 'load', function() {
+					let links = document.querySelectorAll('a[link]');
+					for( let i=0; i<links.length; i++ ) {
+						let link = links[i];
+						link.addEventListener( 'click', function(event) {
+							props.Link = event.target.textContent;
+							mixpanel.owner.track( 'Click', props, {send_immediately:true} );
+						} );
+					}
+				} );
+			}
+		}
+	}
+}
+
+
+// Facebook
+let fbId = isProduction ? '667025338202310' : '1504114567086568';
+let fbOptions = {};
+if( window.UserEmail )
+	fbOptions.em = window.UserEmail.trim().toLowerCase();
+if( cookies.user )
+	fbOptions.external_id = cookies.user;
+//console.log(fbOptions);
+
+!function(f,b,e,v,n,t,s)
+{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
+n.callMethod.apply(n,arguments):n.queue.push(arguments)};
+if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
+n.queue=[];t=b.createElement(e);t.async=!0;
+t.src=v;s=b.getElementsByTagName(e)[0];
+s.parentNode.insertBefore(t,s)}(window, document,'script',
+'https://connect.facebook.net/en_US/fbevents.js');
+fbq('init', fbId, fbOptions);
+
+let fbEvents = null;
+
+function fbSendEvents() {
+	//console.log(fbEvents);
+	let eventID = Math.random().toString();
+	let options = {eventID:eventID};
+	let url = `/facebook.js?event_id=${eventID}`;
+	for( let event of fbEvents ) {
+		let name = event.name;
+		let props = event.properties;
+		fbq( event.action, name, props, options );
+		url += `&event_name=${name}`;
+		if( props )
+			url += `&${name}=${encodeURIComponent(JSON.stringify(props))}`;
+	}
+	ajax(url);
+	fbEvents = null;
+}
+
+function fbTrack(action,name,properties) {
+	if( !fbEvents ) {
+		fbEvents = [];
+		setTimeout(fbSendEvents);
+	}
+	fbEvents.push({
+		action: action,
+		name: name,
+		properties: properties,
+	});
+}
+
+
+function mainSelectHashtag(hashtag) {
+	let style = document.querySelector('style[hashtag]');
+	if( hashtags[hashtag] ) {
+		style.innerHTML = `
+			div[pics] > span {
+				display: none;
+			}
+			div[pics] > span.${hashtag} {
+				display: block;
+			}
+`					;
+		history.replaceState(null,null, `#${hashtag}` );
+	} else {
+		style.innerHTML = '';
+		history.replaceState(null,null, location.pathname + location.search );
+	}
+}
+
+function mainInit() {
+	let hash = location.hash;
+	if( hash ) {
+		hash = hash.slice(1);  // remove '#'
+		if( hashtags[hash] ) {
+			let select = document.querySelector('[hashtags] select');
+			select.value = hash;
+			mainSelectHashtag(hash);
+		} else {
+			history.replaceState(null,null, location.pathname + location.search );
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/theme.css.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,138 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local parse = Luan.parse or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local format_date = Http.format_date or error()
+local User = require "site:/lib/User.luan"
+local get_background_img_url = User.get_background_img_url or error()
+
+
+return function()
+	local user = Http.request.parameters.user or error()
+	user = User.get_by_id(user) or error()
+	local data = user.theme_data or error()
+	data = parse(data)
+	local date = user.theme_date or error()
+	Http.response.headers["Last-Modified"] = format_date(date)
+	Io.stdout = Http.response.text_writer()
+	local font_url = data.font_url
+	if font_url ~= nil then
+%>
+@import "<%=font_url%>";
+
+<%
+	end
+	local background_color = data.background_color
+	if background_color ~= nil then
+%>
+body[colored] {
+	background-color: <%=background_color%>;
+}
+<%
+	end
+	local background_img_url = get_background_img_url(data)
+	if background_img_url ~= nil then
+%>
+div[pub_background_img] {
+	z-index: -1;
+	position: fixed;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	left: 0;
+	background-size: cover;
+	background-repeat: no-repeat;
+	background-position: center;
+	background-image: url(<%=background_img_url%>);
+}
+<%
+	end
+	local link_background_color = data.link_background_color
+	if link_background_color ~= nil then
+%>
+a[link],
+span[select] select {
+	background-color: <%=link_background_color%>;
+}
+<%
+	end
+	local link_hover_background_color = data.link_hover_background_color
+	if link_hover_background_color ~= nil then
+%>
+a[link]:hover,
+span[select] select:hover {
+	background-color: <%=link_hover_background_color%>;
+}
+<%
+	end
+	local link_text_color = data.link_text_color
+	if link_text_color ~= nil then
+%>
+a[link],
+span[select] {
+	color: <%=link_text_color%>;
+}
+<%
+	end
+	local title_color = data.title_color
+	if title_color ~= nil then
+%>
+html[main] h1 {
+	color: <%=title_color%>;
+}
+html[main] div[home] a {
+	color: <%=title_color%>;
+}
+<%
+	end
+	local bio_color = data.bio_color
+	if bio_color ~= nil then
+%>
+html[main] div[bio] {
+	color: <%=bio_color%>;
+}
+<%
+	end
+	if data.icon_color == "white" then
+%>
+html[pic] div[hashtags] a,
+html[pic] div[back] img,
+html[main] div[icons] img {
+	filter: invert(100%);
+}
+<%
+	end
+	local link_border_radius = data.link_border_radius
+	if link_border_radius ~= nil then
+%>
+a[link] {
+	border-radius: <%=link_border_radius%>;
+}
+<%
+	end
+	local link_border_color = data.link_border_color
+	if link_border_color ~= nil then
+%>
+a[link] {
+	border: 2px solid <%=link_border_color%>;
+}
+<%
+	end
+	local link_shadow = data.link_shadow
+	if link_shadow ~= nil then
+%>
+a[link] {
+	box-shadow: <%=link_shadow%> <%= data.link_shadow_color or "" %>;
+}
+<%
+	end
+	local font = data.font
+	if font ~= nil then
+%>
+[pub_content] {
+	font-family: <%=font%>;
+}
+<%
+	end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/theme.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,353 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local pairs = Luan.pairs or error()
+local parse = Luan.parse or error()
+local Html = require "luan:Html.luan"
+local html_encode = Html.encode or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local body_header = Shared.body_header or error()
+local footer = Shared.footer or error()
+local fields = Shared.theme_fields or error()
+local User = require "site:/lib/User.luan"
+local get_background_img_url = User.get_background_img_url or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "theme.html"
+
+
+local google_explanation = [[All Google fonts use this URL format.  You can change the font name and edit the font URL to use other Google fonts not listed here.]]
+
+local fonts = {
+	["Sans-Serif"] = {}
+	["Times New Roman"] = {}
+	["Optima"] = {}
+	["Apple Chancery"] = {}
+	["Courier"] = {}
+	["Copperplate"] = {}
+	["Bitter"] = {
+		title = "Google: Bitter"
+		url = "https://fonts.googleapis.com/css?family=Bitter"
+		explanation = google_explanation
+	}
+	["Genos"] = {
+		title = "Google: Genos"
+		url = "https://fonts.googleapis.com/css?family=Genos"
+		explanation = google_explanation
+	}
+	["Emblema One"] = {
+		title = "Google: Emblema One"
+		url = "https://fonts.googleapis.com/css?family=Emblema+One"
+		explanation = google_explanation
+	}
+}
+for name, font in pairs(fonts) do
+	font.name = name
+	font.title = font.title or name
+	font.url = font.url or ""
+	font.explanation = font.explanation or ""
+end
+
+local function color_input(user_data,color)
+	local value = user_data[color]
+	value = value and html_encode(value)
+	local default = fields[color]
+	local v = value or default
+%>
+				<div>
+					<input type=color <%= v=="" and "" or [[value="]]..v..[["]] %> oninput="colorChange(this)">
+					<span color <%= v=="" and "" or [[style="background-color:]]..v..[["]] %> onclick="colorClick(this)"></span>
+					<input type=text name="<%=color%>" value="<%= value or "" %>" placeholder="<%=default%>" onchange="colorInputChange(this)">
+				</div>
+<%
+end
+
+local function radio_input(user_data,name,value)
+	local current = user_data[name] or fields[name]
+	local checked = value==current and "checked" or ""
+	%><input type=radio name="<%=name%>" value="<%=value%>" <%=checked%> ><%
+end
+
+return function()
+	local user = User.current_required()
+	if user==nil then return end
+	local user_data = user.theme_data
+	user_data = user_data and parse(user_data) or {}
+	local message
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+		<style>
+<%
+	for _, font in pairs(fonts) do
+		if font.url ~= "" then
+%>
+			@import "<%=font.url%>";
+<%
+		end
+%>
+<%
+	end
+%>
+
+			h1 {
+				text-align: center;
+			}
+			div[body] {
+				max-width: 600px;
+				margin-left: auto;
+				margin-right: auto;
+			}
+			@media (max-width: 700px) {
+				div[body] {
+					max-width: 90%;
+				}
+			}
+			label {
+				display: block;
+			}
+			form > div {
+				display: flex;
+				margin-top: 1px;
+				margin-bottom: 10px;
+				position: relative;
+			}
+			input[type="color"] {
+				/* hide this useless modern piece of junk */
+				position: absolute;
+				z-index: 1;
+				width: 42.5px;
+				height: 42.5px;
+				cursor: pointer;
+				opacity: 0;
+			}
+			span[color] {
+				width: 42.5px;
+				cursor: pointer;
+				border: 1px solid #E0E0E0;
+			}
+			form span[pulldown] {
+				border: 1px solid #E0E0E0;
+				display: flex;
+				align-items: center;
+			}
+			form span[pulldown] > span {
+				padding: 4px;
+			}
+			span[shape] a {
+				margin: 12px;
+				padding: 12px 100px;
+				border: 1px solid black;
+			}
+			span[shape] a:hover {
+				text-decoration: none;
+			}
+			input[type="text"] {
+				display: initial;
+				margin: 0;
+				border-radius: 0;
+			}
+			label[clickable] {
+				border: 1px solid grey;
+				padding: 4px;
+				padding-right: 8px;
+			}
+			label[white] {
+				color: white;
+				background-color: black;
+			}
+			div[background_img] {
+				gap: 8px;
+			}
+			button[background_img] {
+				width: 42.5px;
+				height: 42.5px;
+				padding: 0;
+				border: 1px solid #E0E0E0;
+			}
+			button[background_img] img {
+				display: block;
+				width: 100%;
+				height: 100%;
+				object-fit: cover;
+			}
+			div[background_img] button[small] {
+				flex-grow: 1;
+				background-color: #E0E0E0;
+				color: black;
+			}
+			div[background_img] button[small]:hover {
+				background-color: #C0C0C0;
+			}
+			div[font] {
+				gap: 8px;
+			}
+			div[font] input[name="font"] {
+				width: 33%;
+			}
+			div[font] input[name="font_url"] {
+				width: 66%;
+			}
+			div[font_explanation] {
+				color: #808080;
+			}
+		</style>
+		<script>
+			'use strict';
+
+			function colorClick(colorSpan) {
+				console.log('colorClick');
+				let colorInput = colorSpan.parentNode.querySelector('input[type="color"]');
+				colorInput.click();
+			}
+			function colorChange(colorInput) {
+				let parent = colorInput.parentNode;
+				let color = colorInput.value;
+				parent.querySelector('input[type="text"]').value = color;
+				parent.querySelector('span[color]').style['background-color'] = color;
+			}
+			function colorInputChange(input) {
+				let parent = input.parentNode;
+				let color = input.value || input.placeholder;
+				parent.querySelector('input[type="color"]').value = color;
+				parent.querySelector('span[color]').style['background-color'] = color;
+			}
+			function setField(name,value) {
+				document.querySelector('input[type="text"][name="'+name+'"]').value = value;
+			}
+			function setFont(font,url,explanation) {
+				setField('font',font);
+				setField('font_url',url);
+				document.querySelector('div[font_explanation]').textContent = explanation;
+			}
+
+			function uploaded(uuid,filename) {
+				document.querySelector('input[name="background_img_uuid"]').value = uuid;
+				document.querySelector('input[name="background_img_filename"]').value = filename;
+				document.querySelector('div[background_img] img').src = uploadcareUrl(uuid);
+			}
+			function clearImg() {
+				document.querySelector('input[name="background_img_uuid"]').value = '';
+				document.querySelector('input[name="background_img_filename"]').value = '';
+				document.querySelector('div[background_img] img').src = '/images/nothing.svg';
+			}
+		</script>
+	</head>
+	<body>
+	<div full>
+<%		body_header() %>
+		<h1>My Theme</h1>
+		<p top>Click the left side of every section to make quick changes or put exactly what you'd like in the input field. To remove a color or shadow, delete the text from the input field.</p>
+		<div body>
+			<form onsubmit="ajaxForm('/theme.js',this)" action="javascript:">
+				<label>Background Image</label>
+				<div background_img>
+					<button type=button background_img onclick="uploadcare.upload(uploaded)">
+						<img src="<%= get_background_img_url(user_data) or "/images/nothing.svg" %>" >
+					</button>
+					<input type=hidden name="background_img_uuid" value="<%= user_data.background_img_uuid or "" %>">
+					<input type=hidden name="background_img_filename" value="<%= user_data.background_img_filename or "" %>">
+					<button type=button small onclick="uploadcare.upload(uploaded)">Add</button>
+					<button type=button small onclick="clearImg()">Clear</button>
+				</div>
+				<label>Background Color</label>
+<%				color_input(user_data,"background_color") %>
+				<label>Button Color</label>
+<%				color_input(user_data,"link_background_color") %>
+				<label>Button Hover Color</label>
+<%				color_input(user_data,"link_hover_background_color") %>
+				<label>Button Text Color</label>
+<%				color_input(user_data,"link_text_color") %>
+				<label>Title Color</label>
+<%				color_input(user_data,"title_color") %>
+				<label>Bio Color</label>
+<%				color_input(user_data,"bio_color") %>
+				<label>Social Icon Color</label>
+				<div>
+					<label clickable black><% radio_input(user_data,"icon_color","") %>Black</label>
+					&nbsp; &nbsp;
+					<label clickable white><% radio_input(user_data,"icon_color","white") %>White</label>
+				</div>
+				<label>Button Shape</label>
+<%
+	local field = "link_border_radius"
+	local value = user_data[field]
+	value = value and html_encode(value)
+	local default = fields[field]
+	local value_or_default = value or default
+%>
+				<div>
+					<span shape pulldown>
+						<span onclick="clickMenu(this)">Select...</span>
+						<div pulldown_menu>
+							<a style="border-radius:22px / 50%" href="javascript:setField('<%=field%>','22px / 50%')">Link</a>
+							<a style="border-radius:12px / 50%" href="javascript:setField('<%=field%>','12px / 50%')">Link</a>
+							<a style="border-radius:10px" href="javascript:setField('<%=field%>','10px')">Link</a>
+							<a style="border-radius:0" href="javascript:setField('<%=field%>','0')">Link</a>
+							<a style="border-radius:10px 100px" href="javascript:setField('<%=field%>','10px 100px')">Link</a>
+						</div>
+					</span>
+					<input type=text name="<%=field%>" value="<%= value or "" %>" placeholder="<%=default%>">
+				</div>
+				<label>Button Border Color</label>
+<%				color_input(user_data,"link_border_color") %>
+				<label>Button Shadow</label>
+<%
+	local field = "link_shadow"
+	local value = user_data[field]
+	value = value and html_encode(value)
+	local default = fields[field]
+	local value_or_default = value or default
+%>
+				<div>
+					<span shape pulldown>
+						<span onclick="clickMenu(this)">Select...</span>
+						<div pulldown_menu>
+							<a style="box-shadow:5px 5px 2px 1px" href="javascript:setField('<%=field%>','5px 5px 2px 1px')">Link</a>
+							<a style="box-shadow:1px 1px 10px 1px" href="javascript:setField('<%=field%>','1px 1px 10px 1px')">Link</a>
+						</div>
+					</span>
+					<input type=text name="<%=field%>" value="<%= value or "" %>" placeholder="<%=default%>">
+				</div>
+				<label>Button Shadow Color</label>
+<%				color_input(user_data,"link_shadow_color") %>
+				<label>Font</label>
+<%
+	local value = user_data.font
+	value = value and html_encode(value)
+	local value_url = user_data.font_url
+	value_url = value_url and html_encode(value_url)
+	local default = fields.font
+	local explanation = fonts[value] and fonts[value].explanation or ""
+%>
+				<div font>
+					<span pulldown>
+						<span onclick="clickMenu(this)">Select...</span>
+						<div pulldown_menu>
+<%	for _, font in pairs(fonts) do %>
+							<a style="font-family:<%=font.name%>" href="javascript:setFont('<%=font.name%>','<%=font.url%>','<%=font.explanation%>')"><%=font.title%></a>
+<%	end %>
+						</div>
+					</span>
+					<input type=text name="font" value="<%= value or "" %>" placeholder="<%=default%>">
+					<input type=text name="font_url" value="<%= value_url or "" %>" placeholder="URL">
+				</div>
+				<div font_explanation><%=explanation%></div>
+				<button type=submit big>Save</button>
+<%
+	local msg = message and message.info
+%>
+				<div success><%= msg or "" %></div>
+			</form>
+		</div>
+<%		footer() %>
+	</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/theme.js.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,48 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local pairs = Luan.pairs or error()
+local stringify = Luan.stringify or error()
+local String = require "luan:String.luan"
+local trim = String.trim or error()
+local Table = require "luan:Table.luan"
+local is_empty = Table.is_empty or error()
+local Time = require "luan:Time.luan"
+local time_now = Time.now or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local User = require "site:/lib/User.luan"
+local Shared = require "site:/lib/Shared.luan"
+local fields = Shared.theme_fields or error()
+local compressed = Shared.compressed or error()
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "theme.js"
+
+
+return function()
+	local user = User.current() or error()
+	local data = {}
+	for f in pairs(fields) do
+		local v = Http.request.parameters[f] or error()
+		v = trim(v)
+		if v ~= "" then
+			data[f] = v
+		end
+	end
+	run_in_transaction( function()
+		user = user.reload()
+		if is_empty(data) then
+			user.theme_data = nil
+			user.theme_date = nil
+		else
+			user.theme_data = stringify(data,compressed)
+			user.theme_date = time_now()
+		end
+		user.save()
+	end )
+	Io.stdout = Http.response.text_writer()
+%>
+	location = '/<%=user.name%>?saved';
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tools/animation.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,349 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<script>
+			function resized() {
+				let imgs = document.querySelectorAll('img[rect]');
+				for( let img of imgs ) {
+					img.style['border-radius'] = 0.3 * img.width + 'px';
+				}
+			}
+			function loaded() {
+				resized();
+				let imgs = document.querySelectorAll('img');
+				for( let img of imgs ) {
+					img.style.visibility = 'visible';
+					img.style['animation-play-state'] = 'running';
+				}
+			}
+		</script>
+		<style>
+			body {
+				display: flex;
+				justify-content: center;
+				align-items: center;
+				height: 100vh;
+				margin: 0;
+			}
+			div {
+				width: 90%;
+				aspect-ratio: 950 / 535;
+				background-color: rgb(138, 127, 210);
+				position: relative;
+			}
+			div:hover img {
+				animation-play-state: paused;
+			}
+			img {
+				position: absolute;
+				top: 50%;
+				left: 50%;
+				transform: translate(-50%,-50%);
+				animation-duration: 7s;
+				animation-iteration-count: infinite;
+				box-shadow: 5px 5px 20px 1px rgba(0,0,0,0.5);
+				visibility: hidden;
+				animation-play-state: paused;
+			}
+			img[circle] {
+				border-radius: 50%;
+			}
+			img[bio] {
+				animation-name: bio;
+				width: 12%;
+			}
+			@keyframes bio {
+				0% {
+					opacity: 0;
+					transform: translate(-50%,-50%) scale(1);
+				}
+				15% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(1.4375);
+				}
+				20%, 50% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(1);
+				}
+				60% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(1.1875);
+				}
+				65% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(0.875);
+				}
+				70% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(1.1875);
+				}
+				75% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(0.875);
+				}
+				80%, 100% {
+					opacity: 1;
+					transform: translate(-50%,-50%) scale(1);
+				}
+			}
+			img[i1] {
+				animation-name: i1;
+				width: 14%;
+			}
+			@keyframes i1 {
+				0%, 28% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				38% {
+					opacity: 0.1;
+				}
+				48%, 78% {
+					opacity: 1;
+					top: 61%;
+					left: 89%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			img[i2] {
+				animation-name: i2;
+				width: 17%;
+			}
+			@keyframes i2 {
+				0%, 30% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				40% {
+					opacity: 0.1;
+				}
+				50%, 80% {
+					opacity: 1;
+					top: 74%;
+					left: 69%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			img[i3] {
+				animation-name: i3;
+				width: 15%;
+			}
+			@keyframes i3 {
+				0%, 26% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				36% {
+					opacity: 0.1;
+				}
+				46%, 76% {
+					opacity: 1;
+					top: 25%;
+					left: 75%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			img[i4] {
+				animation-name: i4;
+				width: 13%;
+			}
+			@keyframes i4 {
+				0%, 20% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				30% {
+					opacity: 0.1;
+				}
+				40%, 70% {
+					opacity: 1;
+					top: 76%;
+					left: 32%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			img[i5] {
+				animation-name: i5;
+				width: 16%;
+			}
+			@keyframes i5 {
+				0%, 22% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				32% {
+					opacity: 0.1;
+				}
+				42%, 72% {
+					opacity: 1;
+					top: 56%;
+					left: 12%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			img[i6] {
+				animation-name: i6;
+				width: 14%;
+			}
+			@keyframes i6 {
+				0%, 24% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				24% {
+					opacity: 0.1;
+				}
+				44%, 74% {
+					opacity: 1;
+					top: 20%;
+					left: 26%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			img[ins] {
+				animation-name: ins;
+				width: 7%;
+			}
+			@keyframes ins {
+				0%, 30% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				40% {
+					opacity: 0.1;
+				}
+				50%, 80% {
+					opacity: 1;
+					top: 25%;
+					left: 58%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			img[tk] {
+				animation-name: tk;
+				width: 7%;
+			}
+			@keyframes tk {
+				0%, 32% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				42% {
+					opacity: 0.1;
+				}
+				52%, 82% {
+					opacity: 1;
+					top: 45%;
+					left: 35%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+			img[yt] {
+				animation-name: yt;
+				width: 7%;
+			}
+			@keyframes yt {
+				0%, 34% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+				44% {
+					opacity: 0.1;
+				}
+				54%, 84% {
+					opacity: 1;
+					top: 23%;
+					left: 43%;
+				}
+				85% {
+					opacity: 0.1;
+				}
+				90%, 100% {
+					opacity: 0;
+					top: 50%;
+					left: 50%;
+				}
+			}
+		</style>
+	</head>
+	<body onload="loaded()" onresize="resized()">
+		<div>
+			<img bio rect src="https://test.linkmy.style/images/home/bio.png">
+			<img i1 rect src="https://test.linkmy.style/images/home/i1.png">
+			<img i2 rect src="https://test.linkmy.style/images/home/i2.png">
+			<img i3 rect src="https://test.linkmy.style/images/home/i3.png">
+			<img i4 rect src="https://test.linkmy.style/images/home/i4.png">
+			<img i5 rect src="https://test.linkmy.style/images/home/i5.png">
+			<img i6 rect src="https://test.linkmy.style/images/home/i6.png">
+			<img ins circle src="https://test.linkmy.style/images/home/ins.png">
+			<img tk circle src="https://test.linkmy.style/images/home/tk.png">
+			<img yt circle src="https://test.linkmy.style/images/home/yt.png">
+		</div>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tools/cookies.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,1 @@
+return require "luan:http/tools/cookies.html.luan"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tools/dimensions.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,21 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<script>
+			'use strict';
+
+			function p() {
+				document.body.innerText = ''
+					+ window.innerWidth + 'px innerWidth\n' 
+					+ window.innerHeight + 'px innerHeight\n'
+					+ '\n'
+					+ window.outerWidth + 'px outerWidth\n' 
+					+ window.outerHeight + 'px outerHeight\n'
+				;
+			}
+		</script>
+	</head>
+	<body onload="p()" onresize="p()">
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tools/request.txt.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,1 @@
+return require "luan:http/tools/request.txt.luan"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tools/tools.css	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,31 @@
+* {
+	box-sizing: border-box;
+}
+
+body {
+	font-family: Sans-Serif;
+}
+
+a {
+	text-decoration: none;
+}
+a:hover {
+	text-decoration: underline;
+}
+
+table {
+	border-collapse: collapse;
+}
+th, td {
+	border: 1px solid;
+	padding: 4px;
+}
+
+button,
+[onclick],
+[clickable],
+input[type="radio"],
+input[type="file"],
+input[type="submit"] {
+	cursor: pointer;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/blocks.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,29 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			lr-file-uploader-regular {
+				--cfg-pubkey: '718cc25ec1508ca5801d';
+				--cfg-multiple: 0;
+				--cfg-img-only: 1;
+				--cfg-store: 0;
+			}
+		</style>
+		<script src="https://unpkg.com/@uploadcare/blocks@0.17.0/web/blocks-browser.min.js" type="module"></script>
+		<script>
+			'use strict';
+		</script>
+	</head>
+	<body>
+		<p>top</p>
+		<p>
+			<lr-file-uploader-regular
+			  css-src="https://unpkg.com/@uploadcare/blocks@0.17.0/web/file-uploader-regular.min.css"
+			>
+			</lr-file-uploader-regular>
+		</p>
+		<p><a></a></p>
+		<p>bottom</p>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/compress.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,44 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			@import "/uploadcare/croppr.css";
+			@import "/uploadcare/uploadcare.css";
+
+			img {
+				width: 50%;
+			}
+		</style>
+		<script src="/uploadcare/croppr.js"></script>
+		<script src="/uploadcare/uploadcare.js"></script>
+		<script>
+			'use strict';
+
+			uploadcare.maxFileSize = 1000000;
+			uploadcare.cropprOptions = {};
+
+			async function loaded(input) {
+				let info = { file: input.files[0] };
+				input.value = null;
+				await uploadcare.infoCompress(info);
+				console.log(info);
+				if( info.canceled )
+					return;
+				document.querySelector('span[original]').textContent = info.file.size;
+				document.querySelector('p[compressed]').textContent = 'compressed: ' + info.compressedName + ' ' + info.compressed.size;
+				await uploadcare.infoAddUrl(info);
+				document.querySelector('img[original]').src = info.url;
+				let info2 = { file: info.compressed };
+				await uploadcare.infoAddUrl(info2);
+				document.querySelector('img[compressed]').src = info2.url;
+			}
+		</script>
+	</head>
+	<body>
+		<p><input type=file accept="image/*" onchange="loaded(this)"> <span original></span></p>
+		<p compressed></p>
+		<p>left is original, right is compressed</p>
+		<p><img original><img compressed></p>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/croppr.css	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,58 @@
+.croppr-container * {
+  user-select: none;
+  -moz-user-select: none;
+  -webkit-user-select: none;
+  -ms-user-select: none;
+  box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+}
+
+.croppr-container img {
+  vertical-align: middle;
+  max-width: 100%;
+}
+
+.croppr {
+  position: relative;
+  display: inline-block;
+}
+
+.croppr-overlay {
+  background: rgba(0,0,0,0.5);
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1;
+  cursor: crosshair;
+}
+
+.croppr-region {
+  border: 1px dashed rgba(0, 0, 0, 0.5);
+  position: absolute;
+  z-index: 3;
+  cursor: move;
+  top: 0;
+}
+
+.croppr-imageClipped {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 2;
+  pointer-events: none;
+}
+
+.croppr-handle {
+  border: 1px solid black;
+  background-color: white;
+  width: 10px;
+  height: 10px;
+  position: absolute;
+  z-index: 4;
+  top: 0;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/croppr.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,81 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			@import "croppr.css";
+
+			xdialog[croppr] {
+				height: 200px;
+			}
+			xdialog[croppr] div[img] {
+				height: 400px;
+				max-height: 80vh;
+			}
+			dialog[croppr] img {
+				xwidth: 200px;
+				xheight: 100%;
+				height: 400px;
+				max-height: calc(90vh - 110px);
+				display: block;
+			}
+			dialog[croppr] div[buttons] {
+				margin: 8px;
+			}
+			canvas[result] {
+				width: 200px;
+			}
+		</style>
+		<script src="croppr.js"></script>
+		<script>
+			let croppr;
+			function start() {
+				let dialog = document.querySelector('dialog');
+				dialog.showModal();
+				let img = dialog.querySelector('img');
+				//img.src = 'https://ucarecdn.com/ede0040a-b577-4c2e-aeaf-b7f557ba2747/-/quality/smart/';
+				croppr = new Croppr(img, {
+					aspectRatio: 1,
+					//startSize: [100, 100, '%'],
+				});
+			}
+			function cancelCrop() {
+				document.querySelector('dialog[open]').close();
+			}
+			function crop() {
+				let value = croppr.getValue();
+				console.log(value);
+				let width = value.width;
+				let height = value.height;
+				croppr.destroy();
+				let dialog = document.querySelector('dialog');
+				let imgCroppr = dialog.querySelector('img[croppr]');
+				console.log(imgCroppr);
+				//let canvas = document.createElement('canvas');
+				let canvas = document.querySelector('canvas[result]');
+				canvas.width = width;
+				canvas.height = height;
+				let ctx = canvas.getContext('2d');
+				ctx.drawImage(imgCroppr, value.x, value.y, width, height, 0, 0, width, height);
+				//let imgResult = document.querySelector('img[result]');
+				//imgResult.url = canvas.toDataURL();
+				dialog.close();
+			}
+		</script>
+	</head>
+	<body>
+		<p>top</p>
+		<p><button onclick="start()">start</button>
+		<dialog croppr onclose="croppr.destroy()">
+			<div img><img croppr src="https://ucarecdn.com/ede0040a-b577-4c2e-aeaf-b7f557ba2747/-/quality/smart/"></div>
+			<div buttons>
+				<button onclick="crop()">crop</button>
+				<button onclick="cancelCrop()">cancel</button>
+			</div>
+		</dialog>
+		<p>result</p>
+		<p><canvas result></canvas></p>
+		<p><img crossorigin="anonymous" result></p>
+		<p>bottom</p>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/croppr.js	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,1189 @@
+/**
+ * Croppr.js
+ * https://github.com/jamesssooi/Croppr.js
+ * 
+ * A JavaScript image cropper that's lightweight, awesome, and has
+ * zero dependencies.
+ * 
+ * (C) 2017 James Ooi. Released under the MIT License.
+ */
+
+(function (global, factory) {
+	typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+	typeof define === 'function' && define.amd ? define(factory) :
+	(global.Croppr = factory());
+}(this, (function () { 'use strict';
+
+(function () {
+  var lastTime = 0;
+  var vendors = ['ms', 'moz', 'webkit', 'o'];
+  for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+    window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
+    window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
+  }
+  if (!window.requestAnimationFrame) window.requestAnimationFrame = function (callback, element) {
+    var currTime = new Date().getTime();
+    var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+    var id = window.setTimeout(function () {
+      callback(currTime + timeToCall);
+    }, timeToCall);
+    lastTime = currTime + timeToCall;
+    return id;
+  };
+  if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function (id) {
+    clearTimeout(id);
+  };
+})();
+(function () {
+  if (typeof window.CustomEvent === "function") return false;
+  function CustomEvent(event, params) {
+    params = params || { bubbles: false, cancelable: false, detail: undefined };
+    var evt = document.createEvent('CustomEvent');
+    evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
+    return evt;
+  }
+  CustomEvent.prototype = window.Event.prototype;
+  window.CustomEvent = CustomEvent;
+})();
+(function (window) {
+  try {
+    new CustomEvent('test');
+    return false;
+  } catch (e) {}
+  function MouseEvent(eventType, params) {
+    params = params || { bubbles: false, cancelable: false };
+    var mouseEvent = document.createEvent('MouseEvent');
+    mouseEvent.initMouseEvent(eventType, params.bubbles, params.cancelable, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+    return mouseEvent;
+  }
+  MouseEvent.prototype = Event.prototype;
+  window.MouseEvent = MouseEvent;
+})(window);
+
+var classCallCheck = function (instance, Constructor) {
+  if (!(instance instanceof Constructor)) {
+    throw new TypeError("Cannot call a class as a function");
+  }
+};
+
+var createClass = function () {
+  function defineProperties(target, props) {
+    for (var i = 0; i < props.length; i++) {
+      var descriptor = props[i];
+      descriptor.enumerable = descriptor.enumerable || false;
+      descriptor.configurable = true;
+      if ("value" in descriptor) descriptor.writable = true;
+      Object.defineProperty(target, descriptor.key, descriptor);
+    }
+  }
+
+  return function (Constructor, protoProps, staticProps) {
+    if (protoProps) defineProperties(Constructor.prototype, protoProps);
+    if (staticProps) defineProperties(Constructor, staticProps);
+    return Constructor;
+  };
+}();
+
+
+
+
+
+
+
+var get = function get(object, property, receiver) {
+  if (object === null) object = Function.prototype;
+  var desc = Object.getOwnPropertyDescriptor(object, property);
+
+  if (desc === undefined) {
+    var parent = Object.getPrototypeOf(object);
+
+    if (parent === null) {
+      return undefined;
+    } else {
+      return get(parent, property, receiver);
+    }
+  } else if ("value" in desc) {
+    return desc.value;
+  } else {
+    var getter = desc.get;
+
+    if (getter === undefined) {
+      return undefined;
+    }
+
+    return getter.call(receiver);
+  }
+};
+
+var inherits = function (subClass, superClass) {
+  if (typeof superClass !== "function" && superClass !== null) {
+    throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
+  }
+
+  subClass.prototype = Object.create(superClass && superClass.prototype, {
+    constructor: {
+      value: subClass,
+      enumerable: false,
+      writable: true,
+      configurable: true
+    }
+  });
+  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
+};
+
+
+
+
+
+
+
+
+
+
+
+var possibleConstructorReturn = function (self, call) {
+  if (!self) {
+    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+  }
+
+  return call && (typeof call === "object" || typeof call === "function") ? call : self;
+};
+
+
+
+
+
+var slicedToArray = function () {
+  function sliceIterator(arr, i) {
+    var _arr = [];
+    var _n = true;
+    var _d = false;
+    var _e = undefined;
+
+    try {
+      for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
+        _arr.push(_s.value);
+
+        if (i && _arr.length === i) break;
+      }
+    } catch (err) {
+      _d = true;
+      _e = err;
+    } finally {
+      try {
+        if (!_n && _i["return"]) _i["return"]();
+      } finally {
+        if (_d) throw _e;
+      }
+    }
+
+    return _arr;
+  }
+
+  return function (arr, i) {
+    if (Array.isArray(arr)) {
+      return arr;
+    } else if (Symbol.iterator in Object(arr)) {
+      return sliceIterator(arr, i);
+    } else {
+      throw new TypeError("Invalid attempt to destructure non-iterable instance");
+    }
+  };
+}();
+
+var Handle =
+/**
+ * Creates a new Handle instance.
+ * @constructor
+ * @param {Array} position The x and y ratio position of the handle
+ *      within the crop region. Accepts a value between 0 to 1 in the order
+ *      of [X, Y].
+ * @param {Array} constraints Define the side of the crop region that
+ *      is to be affected by this handle. Accepts a value of 0 or 1 in the
+ *      order of [TOP, RIGHT, BOTTOM, LEFT].
+ * @param {String} cursor The CSS cursor of this handle.
+ * @param {Element} eventBus The element to dispatch events to.
+ */
+function Handle(position, constraints, cursor, eventBus) {
+  classCallCheck(this, Handle);
+  var self = this;
+  this.position = position;
+  this.constraints = constraints;
+  this.cursor = cursor;
+  this.eventBus = eventBus;
+  this.el = document.createElement('div');
+  this.el.className = 'croppr-handle';
+  this.el.style.cursor = cursor;
+  this.el.addEventListener('mousedown', onMouseDown);
+  function onMouseDown(e) {
+    e.stopPropagation();
+    document.addEventListener('mouseup', onMouseUp);
+    document.addEventListener('mousemove', onMouseMove);
+    self.eventBus.dispatchEvent(new CustomEvent('handlestart', {
+      detail: { handle: self }
+    }));
+  }
+  function onMouseUp(e) {
+    e.stopPropagation();
+    document.removeEventListener('mouseup', onMouseUp);
+    document.removeEventListener('mousemove', onMouseMove);
+    self.eventBus.dispatchEvent(new CustomEvent('handleend', {
+      detail: { handle: self }
+    }));
+  }
+  function onMouseMove(e) {
+    e.stopPropagation();
+    self.eventBus.dispatchEvent(new CustomEvent('handlemove', {
+      detail: { mouseX: e.clientX, mouseY: e.clientY }
+    }));
+  }
+};
+
+var Box = function () {
+  /**
+   * Creates a new Box instance.
+   * @constructor
+   * @param {Number} x1
+   * @param {Number} y1
+   * @param {Number} x2
+   * @param {Number} y2
+   */
+  function Box(x1, y1, x2, y2) {
+    classCallCheck(this, Box);
+    this.x1 = x1;
+    this.y1 = y1;
+    this.x2 = x2;
+    this.y2 = y2;
+  }
+  /**
+   * Sets the new dimensions of the box.
+   * @param {Number} x1
+   * @param {Number} y1
+   * @param {Number} x2
+   * @param {Number} y2
+   */
+  createClass(Box, [{
+    key: 'set',
+    value: function set$$1() {
+      var x1 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
+      var y1 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
+      var x2 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
+      var y2 = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
+      this.x1 = x1 == null ? this.x1 : x1;
+      this.y1 = y1 == null ? this.y1 : y1;
+      this.x2 = x2 == null ? this.x2 : x2;
+      this.y2 = y2 == null ? this.y2 : y2;
+      return this;
+    }
+    /**
+     * Calculates the width of the box.
+     * @returns {Number}
+     */
+  }, {
+    key: 'width',
+    value: function width() {
+      return Math.abs(this.x2 - this.x1);
+    }
+    /**
+     * Calculates the height of the box.
+     * @returns {Number}
+     */
+  }, {
+    key: 'height',
+    value: function height() {
+      return Math.abs(this.y2 - this.y1);
+    }
+    /**
+     * Resizes the box to a new size.
+     * @param {Number} newWidth
+     * @param {Number} newHeight
+     * @param {Array} [origin] The origin point to resize from.
+     *      Defaults to [0, 0] (top left).
+     */
+  }, {
+    key: 'resize',
+    value: function resize(newWidth, newHeight) {
+      var origin = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0];
+      var fromX = this.x1 + this.width() * origin[0];
+      var fromY = this.y1 + this.height() * origin[1];
+      this.x1 = fromX - newWidth * origin[0];
+      this.y1 = fromY - newHeight * origin[1];
+      this.x2 = this.x1 + newWidth;
+      this.y2 = this.y1 + newHeight;
+      return this;
+    }
+    /**
+     * Scale the box by a factor.
+     * @param {Number} factor
+     * @param {Array} [origin] The origin point to resize from.
+     *      Defaults to [0, 0] (top left).
+     */
+  }, {
+    key: 'scale',
+    value: function scale(factor) {
+      var origin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [0, 0];
+      var newWidth = this.width() * factor;
+      var newHeight = this.height() * factor;
+      this.resize(newWidth, newHeight, origin);
+      return this;
+    }
+  }, {
+    key: 'move',
+    value: function move() {
+      var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
+      var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
+      var width = this.width();
+      var height = this.height();
+      x = x === null ? this.x1 : x;
+      y = y === null ? this.y1 : y;
+      this.x1 = x;
+      this.y1 = y;
+      this.x2 = x + width;
+      this.y2 = y + height;
+      return this;
+    }
+    /**
+     * Get relative x and y coordinates of a given point within the box.
+     * @param {Array} point The x and y ratio position within the box.
+     * @returns {Array} The x and y coordinates [x, y].
+     */
+  }, {
+    key: 'getRelativePoint',
+    value: function getRelativePoint() {
+      var point = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0];
+      var x = this.width() * point[0];
+      var y = this.height() * point[1];
+      return [x, y];
+    }
+    /**
+     * Get absolute x and y coordinates of a given point within the box.
+     * @param {Array} point The x and y ratio position within the box.
+     * @returns {Array} The x and y coordinates [x, y].
+     */
+  }, {
+    key: 'getAbsolutePoint',
+    value: function getAbsolutePoint() {
+      var point = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0];
+      var x = this.x1 + this.width() * point[0];
+      var y = this.y1 + this.height() * point[1];
+      return [x, y];
+    }
+    /**
+     * Constrain the box to a fixed ratio.
+     * @param {Number} ratio
+     * @param {Array} [origin] The origin point to resize from.
+     *     Defaults to [0, 0] (top left).
+     * @param {String} [grow] The axis to grow to maintain the ratio.
+     *     Defaults to 'height'.
+     */
+  }, {
+    key: 'constrainToRatio',
+    value: function constrainToRatio(ratio) {
+      var origin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [0, 0];
+      var grow = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'height';
+      if (ratio === null) {
+        return;
+      }
+      var width = this.width();
+      var height = this.height();
+      switch (grow) {
+        case 'height':
+          this.resize(this.width(), this.width() * ratio, origin);
+          break;
+        case 'width':
+          this.resize(this.height() * 1 / ratio, this.height(), origin);
+          break;
+        default:
+          this.resize(this.width(), this.width() * ratio, origin);
+      }
+      return this;
+    }
+    /**
+     * Constrain the box within a boundary.
+     * @param {Number} boundaryWidth
+     * @param {Number} boundaryHeight
+     * @param {Array} [origin] The origin point to resize from.
+     *     Defaults to [0, 0] (top left).
+     */
+  }, {
+    key: 'constrainToBoundary',
+    value: function constrainToBoundary(boundaryWidth, boundaryHeight) {
+      var origin = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0];
+      var _getAbsolutePoint = this.getAbsolutePoint(origin),
+          _getAbsolutePoint2 = slicedToArray(_getAbsolutePoint, 2),
+          originX = _getAbsolutePoint2[0],
+          originY = _getAbsolutePoint2[1];
+      var maxIfLeft = originX;
+      var maxIfTop = originY;
+      var maxIfRight = boundaryWidth - originX;
+      var maxIfBottom = boundaryHeight - originY;
+      var directionX = -2 * origin[0] + 1;
+      var directionY = -2 * origin[1] + 1;
+      var maxWidth = null,
+          maxHeight = null;
+      switch (directionX) {
+        case -1:
+          maxWidth = maxIfLeft;break;
+        case 0:
+          maxWidth = Math.min(maxIfLeft, maxIfRight) * 2;break;
+        case +1:
+          maxWidth = maxIfRight;break;
+      }
+      switch (directionY) {
+        case -1:
+          maxHeight = maxIfTop;break;
+        case 0:
+          maxHeight = Math.min(maxIfTop, maxIfBottom) * 2;break;
+        case +1:
+          maxHeight = maxIfBottom;break;
+      }
+      if (this.width() > maxWidth) {
+        var factor = maxWidth / this.width();
+        this.scale(factor, origin);
+      }
+      if (this.height() > maxHeight) {
+        var _factor = maxHeight / this.height();
+        this.scale(_factor, origin);
+      }
+      return this;
+    }
+    /**
+     * Constrain the box to a maximum/minimum size.
+     * @param {Number} [maxWidth]
+     * @param {Number} [maxHeight]
+     * @param {Number} [minWidth]
+     * @param {Number} [minHeight]
+     * @param {Array} [origin] The origin point to resize from.
+     *     Defaults to [0, 0] (top left).
+     * @param {Number} [ratio] Ratio to maintain.
+     */
+  }, {
+    key: 'constrainToSize',
+    value: function constrainToSize() {
+      var maxWidth = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
+      var maxHeight = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
+      var minWidth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
+      var minHeight = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
+      var origin = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : [0, 0];
+      var ratio = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null;
+      if (ratio) {
+        if (ratio > 1) {
+          maxWidth = maxHeight * 1 / ratio;
+          minHeight = minHeight * ratio;
+        } else if (ratio < 1) {
+          maxHeight = maxWidth * ratio;
+          minWidth = minHeight * 1 / ratio;
+        }
+      }
+      if (maxWidth && this.width() > maxWidth) {
+        var newWidth = maxWidth,
+            newHeight = ratio === null ? this.height() : maxHeight;
+        this.resize(newWidth, newHeight, origin);
+      }
+      if (maxHeight && this.height() > maxHeight) {
+        var _newWidth = ratio === null ? this.width() : maxWidth,
+            _newHeight = maxHeight;
+        this.resize(_newWidth, _newHeight, origin);
+      }
+      if (minWidth && this.width() < minWidth) {
+        var _newWidth2 = minWidth,
+            _newHeight2 = ratio === null ? this.height() : minHeight;
+        this.resize(_newWidth2, _newHeight2, origin);
+      }
+      if (minHeight && this.height() < minHeight) {
+        var _newWidth3 = ratio === null ? this.width() : minWidth,
+            _newHeight3 = minHeight;
+        this.resize(_newWidth3, _newHeight3, origin);
+      }
+      return this;
+    }
+  }]);
+  return Box;
+}();
+
+/**
+ * Binds an element's touch events to be simulated as mouse events.
+ * @param {Element} element
+ */
+function enableTouch(element) {
+  element.addEventListener('touchstart', simulateMouseEvent);
+  element.addEventListener('touchend', simulateMouseEvent);
+  element.addEventListener('touchmove', simulateMouseEvent);
+}
+/**
+ * Translates a touch event to a mouse event.
+ * @param {Event} e
+ */
+function simulateMouseEvent(e) {
+  e.preventDefault();
+  var touch = e.changedTouches[0];
+  var eventMap = {
+    'touchstart': 'mousedown',
+    'touchmove': 'mousemove',
+    'touchend': 'mouseup'
+  };
+  touch.target.dispatchEvent(new MouseEvent(eventMap[e.type], {
+    bubbles: true,
+    cancelable: true,
+    view: window,
+    clientX: touch.clientX,
+    clientY: touch.clientY,
+    screenX: touch.screenX,
+    screenY: touch.screenY
+  }));
+}
+
+/**
+ * Define a list of handles to create.
+ *
+ * @property {Array} position - The x and y ratio position of the handle within
+ *      the crop region. Accepts a value between 0 to 1 in the order of [X, Y].
+ * @property {Array} constraints - Define the side of the crop region that is to
+ *      be affected by this handle. Accepts a value of 0 or 1 in the order of
+ *      [TOP, RIGHT, BOTTOM, LEFT].
+ * @property {String} cursor - The CSS cursor of this handle.
+ */
+var HANDLES = [{ position: [0.0, 0.0], constraints: [1, 0, 0, 1], cursor: 'nw-resize' }, { position: [0.5, 0.0], constraints: [1, 0, 0, 0], cursor: 'n-resize' }, { position: [1.0, 0.0], constraints: [1, 1, 0, 0], cursor: 'ne-resize' }, { position: [1.0, 0.5], constraints: [0, 1, 0, 0], cursor: 'e-resize' }, { position: [1.0, 1.0], constraints: [0, 1, 1, 0], cursor: 'se-resize' }, { position: [0.5, 1.0], constraints: [0, 0, 1, 0], cursor: 's-resize' }, { position: [0.0, 1.0], constraints: [0, 0, 1, 1], cursor: 'sw-resize' }, { position: [0.0, 0.5], constraints: [0, 0, 0, 1], cursor: 'w-resize' }];
+var CropprCore = function () {
+  function CropprCore(element, options) {
+    var _this = this;
+    var deferred = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+    classCallCheck(this, CropprCore);
+    this.options = CropprCore.parseOptions(options || {});
+    if (!element.nodeName) {
+      element = document.querySelector(element);
+      if (element == null) {
+        throw 'Unable to find element.';
+      }
+    }
+    if (!element.getAttribute('src')) {
+      throw 'Image src not provided.';
+    }
+    this._initialized = false;
+    this._restore = {
+      parent: element.parentNode,
+      element: element
+    };
+    if (!deferred) {
+      if (element.width === 0 || element.height === 0) {
+        element.onload = function () {
+          _this.initialize(element);
+        };
+      } else {
+        this.initialize(element);
+      }
+    }
+  }
+  createClass(CropprCore, [{
+    key: 'initialize',
+    value: function initialize(element) {
+      this.createDOM(element);
+      this.options.convertToPixels(this.cropperEl);
+      this.attachHandlerEvents();
+      this.attachRegionEvents();
+      this.attachOverlayEvents();
+      this.box = this.initializeBox(this.options);
+      this.redraw();
+      this._initialized = true;
+      if (this.options.onInitialize !== null) {
+        this.options.onInitialize(this);
+      }
+    }
+  }, {
+    key: 'createDOM',
+    value: function createDOM(targetEl) {
+      this.containerEl = document.createElement('div');
+      this.containerEl.className = 'croppr-container';
+      this.eventBus = this.containerEl;
+      enableTouch(this.containerEl);
+      this.cropperEl = document.createElement('div');
+      this.cropperEl.className = 'croppr';
+      this.imageEl = document.createElement('img');
+      this.imageEl.setAttribute('src', targetEl.getAttribute('src'));
+      this.imageEl.setAttribute('alt', targetEl.getAttribute('alt'));
+      this.imageEl.className = 'croppr-image';
+      this.imageClippedEl = this.imageEl.cloneNode();
+      this.imageClippedEl.className = 'croppr-imageClipped';
+      this.regionEl = document.createElement('div');
+      this.regionEl.className = 'croppr-region';
+      this.overlayEl = document.createElement('div');
+      this.overlayEl.className = 'croppr-overlay';
+      var handleContainerEl = document.createElement('div');
+      handleContainerEl.className = 'croppr-handleContainer';
+      this.handles = [];
+      for (var i = 0; i < HANDLES.length; i++) {
+        var handle = new Handle(HANDLES[i].position, HANDLES[i].constraints, HANDLES[i].cursor, this.eventBus);
+        this.handles.push(handle);
+        handleContainerEl.appendChild(handle.el);
+      }
+      this.cropperEl.appendChild(this.imageEl);
+      this.cropperEl.appendChild(this.imageClippedEl);
+      this.cropperEl.appendChild(this.regionEl);
+      this.cropperEl.appendChild(this.overlayEl);
+      this.cropperEl.appendChild(handleContainerEl);
+      this.containerEl.appendChild(this.cropperEl);
+      targetEl.parentElement.replaceChild(this.containerEl, targetEl);
+    }
+    /**
+     * Changes the image src.
+     * @param {String} src
+     */
+  }, {
+    key: 'setImage',
+    value: function setImage(src) {
+      var _this2 = this;
+      this.imageEl.onload = function () {
+        _this2.box = _this2.initializeBox(_this2.options);
+        _this2.redraw();
+      };
+      this.imageEl.src = src;
+      this.imageClippedEl.src = src;
+      return this;
+    }
+  }, {
+    key: 'destroy',
+    value: function destroy() {
+      this._restore.parent.replaceChild(this._restore.element, this.containerEl);
+    }
+    /**
+     * Create a new box region with a set of options.
+     * @param {Object} opts The options.
+     * @returns {Box}
+     */
+  }, {
+    key: 'initializeBox',
+    value: function initializeBox(opts) {
+      var width = opts.startSize.width;
+      var height = opts.startSize.height;
+      var box = new Box(0, 0, width, height);
+      box.constrainToRatio(opts.aspectRatio, [0.5, 0.5]);
+      var min = opts.minSize;
+      var max = opts.maxSize;
+      box.constrainToSize(max.width, max.height, min.width, min.height, [0.5, 0.5], opts.aspectRatio);
+      var parentWidth = this.cropperEl.offsetWidth;
+      var parentHeight = this.cropperEl.offsetHeight;
+      box.constrainToBoundary(parentWidth, parentHeight, [0.5, 0.5]);
+      var x = this.cropperEl.offsetWidth / 2 - box.width() / 2;
+      var y = this.cropperEl.offsetHeight / 2 - box.height() / 2;
+      box.move(x, y);
+      return box;
+    }
+  }, {
+    key: 'redraw',
+    value: function redraw() {
+      var _this3 = this;
+      var width = Math.round(this.box.width()),
+          height = Math.round(this.box.height()),
+          x1 = Math.round(this.box.x1),
+          y1 = Math.round(this.box.y1),
+          x2 = Math.round(this.box.x2),
+          y2 = Math.round(this.box.y2);
+      window.requestAnimationFrame(function () {
+        _this3.regionEl.style.transform = 'translate(' + x1 + 'px, ' + y1 + 'px)';
+        _this3.regionEl.style.width = width + 'px';
+        _this3.regionEl.style.height = height + 'px';
+        _this3.imageClippedEl.style.clip = 'rect(' + y1 + 'px, ' + x2 + 'px, ' + y2 + 'px, ' + x1 + 'px)';
+        var center = _this3.box.getAbsolutePoint([.5, .5]);
+        var xSign = center[0] - _this3.cropperEl.offsetWidth / 2 >> 31;
+        var ySign = center[1] - _this3.cropperEl.offsetHeight / 2 >> 31;
+        var quadrant = (xSign ^ ySign) + ySign + ySign + 4;
+        var foregroundHandleIndex = -2 * quadrant + 8;
+        for (var i = 0; i < _this3.handles.length; i++) {
+          var handle = _this3.handles[i];
+          var handleWidth = handle.el.offsetWidth;
+          var handleHeight = handle.el.offsetHeight;
+          var left = x1 + width * handle.position[0] - handleWidth / 2;
+          var top = y1 + height * handle.position[1] - handleHeight / 2;
+          handle.el.style.transform = 'translate(' + Math.round(left) + 'px, ' + Math.round(top) + 'px)';
+          handle.el.style.zIndex = foregroundHandleIndex == i ? 5 : 4;
+        }
+      });
+    }
+  }, {
+    key: 'attachHandlerEvents',
+    value: function attachHandlerEvents() {
+      var eventBus = this.eventBus;
+      eventBus.addEventListener('handlestart', this.onHandleMoveStart.bind(this));
+      eventBus.addEventListener('handlemove', this.onHandleMoveMoving.bind(this));
+      eventBus.addEventListener('handleend', this.onHandleMoveEnd.bind(this));
+    }
+  }, {
+    key: 'attachRegionEvents',
+    value: function attachRegionEvents() {
+      var eventBus = this.eventBus;
+      var self = this;
+      this.regionEl.addEventListener('mousedown', onMouseDown);
+      eventBus.addEventListener('regionstart', this.onRegionMoveStart.bind(this));
+      eventBus.addEventListener('regionmove', this.onRegionMoveMoving.bind(this));
+      eventBus.addEventListener('regionend', this.onRegionMoveEnd.bind(this));
+      function onMouseDown(e) {
+        e.stopPropagation();
+        document.addEventListener('mouseup', onMouseUp);
+        document.addEventListener('mousemove', onMouseMove);
+        eventBus.dispatchEvent(new CustomEvent('regionstart', {
+          detail: { mouseX: e.clientX, mouseY: e.clientY }
+        }));
+      }
+      function onMouseMove(e) {
+        e.stopPropagation();
+        eventBus.dispatchEvent(new CustomEvent('regionmove', {
+          detail: { mouseX: e.clientX, mouseY: e.clientY }
+        }));
+      }
+      function onMouseUp(e) {
+        e.stopPropagation();
+        document.removeEventListener('mouseup', onMouseUp);
+        document.removeEventListener('mousemove', onMouseMove);
+        eventBus.dispatchEvent(new CustomEvent('regionend', {
+          detail: { mouseX: e.clientX, mouseY: e.clientY }
+        }));
+      }
+    }
+  }, {
+    key: 'attachOverlayEvents',
+    value: function attachOverlayEvents() {
+      var SOUTHEAST_HANDLE_IDX = 4;
+      var self = this;
+      var tmpBox = null;
+      this.overlayEl.addEventListener('mousedown', onMouseDown);
+      function onMouseDown(e) {
+        e.stopPropagation();
+        document.addEventListener('mouseup', onMouseUp);
+        document.addEventListener('mousemove', onMouseMove);
+        var container = self.cropperEl.getBoundingClientRect();
+        var mouseX = e.clientX - container.left;
+        var mouseY = e.clientY - container.top;
+        tmpBox = self.box;
+        self.box = new Box(mouseX, mouseY, mouseX + 1, mouseY + 1);
+        self.eventBus.dispatchEvent(new CustomEvent('handlestart', {
+          detail: { handle: self.handles[SOUTHEAST_HANDLE_IDX] }
+        }));
+      }
+      function onMouseMove(e) {
+        e.stopPropagation();
+        self.eventBus.dispatchEvent(new CustomEvent('handlemove', {
+          detail: { mouseX: e.clientX, mouseY: e.clientY }
+        }));
+      }
+      function onMouseUp(e) {
+        e.stopPropagation();
+        document.removeEventListener('mouseup', onMouseUp);
+        document.removeEventListener('mousemove', onMouseMove);
+        if (self.box.width() === 1 && self.box.height() === 1) {
+          self.box = tmpBox;
+          return;
+        }
+        self.eventBus.dispatchEvent(new CustomEvent('handleend', {
+          detail: { mouseX: e.clientX, mouseY: e.clientY }
+        }));
+      }
+    }
+  }, {
+    key: 'onHandleMoveStart',
+    value: function onHandleMoveStart(e) {
+      var handle = e.detail.handle;
+      var originPoint = [1 - handle.position[0], 1 - handle.position[1]];
+      var _box$getAbsolutePoint = this.box.getAbsolutePoint(originPoint),
+          _box$getAbsolutePoint2 = slicedToArray(_box$getAbsolutePoint, 2),
+          originX = _box$getAbsolutePoint2[0],
+          originY = _box$getAbsolutePoint2[1];
+      this.activeHandle = { handle: handle, originPoint: originPoint, originX: originX, originY: originY };
+      if (this.options.onCropStart !== null) {
+        this.options.onCropStart(this.getValue());
+      }
+    }
+  }, {
+    key: 'onHandleMoveMoving',
+    value: function onHandleMoveMoving(e) {
+      var _e$detail = e.detail,
+          mouseX = _e$detail.mouseX,
+          mouseY = _e$detail.mouseY;
+      var container = this.cropperEl.getBoundingClientRect();
+      mouseX = mouseX - container.left;
+      mouseY = mouseY - container.top;
+      if (mouseX < 0) {
+        mouseX = 0;
+      } else if (mouseX > container.width) {
+        mouseX = container.width;
+      }
+      if (mouseY < 0) {
+        mouseY = 0;
+      } else if (mouseY > container.height) {
+        mouseY = container.height;
+      }
+      var origin = this.activeHandle.originPoint.slice();
+      var originX = this.activeHandle.originX;
+      var originY = this.activeHandle.originY;
+      var handle = this.activeHandle.handle;
+      var TOP_MOVABLE = handle.constraints[0] === 1;
+      var RIGHT_MOVABLE = handle.constraints[1] === 1;
+      var BOTTOM_MOVABLE = handle.constraints[2] === 1;
+      var LEFT_MOVABLE = handle.constraints[3] === 1;
+      var MULTI_AXIS = (LEFT_MOVABLE || RIGHT_MOVABLE) && (TOP_MOVABLE || BOTTOM_MOVABLE);
+      var x1 = LEFT_MOVABLE || RIGHT_MOVABLE ? originX : this.box.x1;
+      var x2 = LEFT_MOVABLE || RIGHT_MOVABLE ? originX : this.box.x2;
+      var y1 = TOP_MOVABLE || BOTTOM_MOVABLE ? originY : this.box.y1;
+      var y2 = TOP_MOVABLE || BOTTOM_MOVABLE ? originY : this.box.y2;
+      x1 = LEFT_MOVABLE ? mouseX : x1;
+      x2 = RIGHT_MOVABLE ? mouseX : x2;
+      y1 = TOP_MOVABLE ? mouseY : y1;
+      y2 = BOTTOM_MOVABLE ? mouseY : y2;
+      var isFlippedX = false,
+          isFlippedY = false;
+      if (LEFT_MOVABLE || RIGHT_MOVABLE) {
+        isFlippedX = LEFT_MOVABLE ? mouseX > originX : mouseX < originX;
+      }
+      if (TOP_MOVABLE || BOTTOM_MOVABLE) {
+        isFlippedY = TOP_MOVABLE ? mouseY > originY : mouseY < originY;
+      }
+      if (isFlippedX) {
+        var tmp = x1;x1 = x2;x2 = tmp;
+        origin[0] = 1 - origin[0];
+      }
+      if (isFlippedY) {
+        var _tmp = y1;y1 = y2;y2 = _tmp;
+        origin[1] = 1 - origin[1];
+      }
+      var box = new Box(x1, y1, x2, y2);
+      if (this.options.aspectRatio) {
+        var ratio = this.options.aspectRatio;
+        var isVerticalMovement = false;
+        if (MULTI_AXIS) {
+          isVerticalMovement = mouseY > box.y1 + ratio * box.width() || mouseY < box.y2 - ratio * box.width();
+        } else if (TOP_MOVABLE || BOTTOM_MOVABLE) {
+          isVerticalMovement = true;
+        }
+        var ratioMode = isVerticalMovement ? 'width' : 'height';
+        box.constrainToRatio(ratio, origin, ratioMode);
+      }
+      var min = this.options.minSize;
+      var max = this.options.maxSize;
+      box.constrainToSize(max.width, max.height, min.width, min.height, origin, this.options.aspectRatio);
+      var parentWidth = this.cropperEl.offsetWidth;
+      var parentHeight = this.cropperEl.offsetHeight;
+      box.constrainToBoundary(parentWidth, parentHeight, origin);
+      this.box = box;
+      this.redraw();
+      if (this.options.onCropMove !== null) {
+        this.options.onCropMove(this.getValue());
+      }
+    }
+  }, {
+    key: 'onHandleMoveEnd',
+    value: function onHandleMoveEnd(e) {
+      if (this.options.onCropEnd !== null) {
+        this.options.onCropEnd(this.getValue());
+      }
+    }
+  }, {
+    key: 'onRegionMoveStart',
+    value: function onRegionMoveStart(e) {
+      var _e$detail2 = e.detail,
+          mouseX = _e$detail2.mouseX,
+          mouseY = _e$detail2.mouseY;
+      var container = this.cropperEl.getBoundingClientRect();
+      mouseX = mouseX - container.left;
+      mouseY = mouseY - container.top;
+      this.currentMove = {
+        offsetX: mouseX - this.box.x1,
+        offsetY: mouseY - this.box.y1
+      };
+      if (this.options.onCropStart !== null) {
+        this.options.onCropStart(this.getValue());
+      }
+    }
+  }, {
+    key: 'onRegionMoveMoving',
+    value: function onRegionMoveMoving(e) {
+      var _e$detail3 = e.detail,
+          mouseX = _e$detail3.mouseX,
+          mouseY = _e$detail3.mouseY;
+      var _currentMove = this.currentMove,
+          offsetX = _currentMove.offsetX,
+          offsetY = _currentMove.offsetY;
+      var container = this.cropperEl.getBoundingClientRect();
+      mouseX = mouseX - container.left;
+      mouseY = mouseY - container.top;
+      this.box.move(mouseX - offsetX, mouseY - offsetY);
+      if (this.box.x1 < 0) {
+        this.box.move(0, null);
+      }
+      if (this.box.x2 > container.width) {
+        this.box.move(container.width - this.box.width(), null);
+      }
+      if (this.box.y1 < 0) {
+        this.box.move(null, 0);
+      }
+      if (this.box.y2 > container.height) {
+        this.box.move(null, container.height - this.box.height());
+      }
+      this.redraw();
+      if (this.options.onCropMove !== null) {
+        this.options.onCropMove(this.getValue());
+      }
+    }
+  }, {
+    key: 'onRegionMoveEnd',
+    value: function onRegionMoveEnd(e) {
+      if (this.options.onCropEnd !== null) {
+        this.options.onCropEnd(this.getValue());
+      }
+    }
+  }, {
+    key: 'getValue',
+    value: function getValue() {
+      var mode = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
+      if (mode === null) {
+        mode = this.options.returnMode;
+      }
+      if (mode == 'real') {
+        var actualWidth = this.imageEl.naturalWidth;
+        var actualHeight = this.imageEl.naturalHeight;
+        var _imageEl$getBoundingC = this.imageEl.getBoundingClientRect(),
+            elementWidth = _imageEl$getBoundingC.width,
+            elementHeight = _imageEl$getBoundingC.height;
+        var factorX = actualWidth / elementWidth;
+        var factorY = actualHeight / elementHeight;
+        return {
+          x: Math.round(this.box.x1 * factorX),
+          y: Math.round(this.box.y1 * factorY),
+          width: Math.round(this.box.width() * factorX),
+          height: Math.round(this.box.height() * factorY)
+        };
+      } else if (mode == 'ratio') {
+        var _imageEl$getBoundingC2 = this.imageEl.getBoundingClientRect(),
+            _elementWidth = _imageEl$getBoundingC2.width,
+            _elementHeight = _imageEl$getBoundingC2.height;
+        return {
+          x: round(this.box.x1 / _elementWidth, 3),
+          y: round(this.box.y1 / _elementHeight, 3),
+          width: round(this.box.width() / _elementWidth, 3),
+          height: round(this.box.height() / _elementHeight, 3)
+        };
+      } else if (mode == 'raw') {
+        return {
+          x: Math.round(this.box.x1),
+          y: Math.round(this.box.y1),
+          width: Math.round(this.box.width()),
+          height: Math.round(this.box.height())
+        };
+      }
+    }
+  }], [{
+    key: 'parseOptions',
+    value: function parseOptions(opts) {
+      var defaults$$1 = {
+        aspectRatio: null,
+        maxSize: { width: null, height: null },
+        minSize: { width: null, height: null },
+        startSize: { width: 100, height: 100, unit: '%' },
+        returnMode: 'real',
+        onInitialize: null,
+        onCropStart: null,
+        onCropMove: null,
+        onCropEnd: null
+      };
+      var aspectRatio = null;
+      if (opts.aspectRatio !== undefined) {
+        if (typeof opts.aspectRatio === 'number') {
+          aspectRatio = opts.aspectRatio;
+        } else if (opts.aspectRatio instanceof Array) {
+          aspectRatio = opts.aspectRatio[1] / opts.aspectRatio[0];
+        }
+      }
+      var maxSize = null;
+      if (opts.maxSize !== undefined && opts.maxSize !== null) {
+        maxSize = {
+          width: opts.maxSize[0] || null,
+          height: opts.maxSize[1] || null,
+          unit: opts.maxSize[2] || 'px'
+        };
+      }
+      var minSize = null;
+      if (opts.minSize !== undefined && opts.minSize !== null) {
+        minSize = {
+          width: opts.minSize[0] || null,
+          height: opts.minSize[1] || null,
+          unit: opts.minSize[2] || 'px'
+        };
+      }
+      var startSize = null;
+      if (opts.startSize !== undefined && opts.startSize !== null) {
+        startSize = {
+          width: opts.startSize[0] || null,
+          height: opts.startSize[1] || null,
+          unit: opts.startSize[2] || '%'
+        };
+      }
+      var onInitialize = null;
+      if (typeof opts.onInitialize === 'function') {
+        onInitialize = opts.onInitialize;
+      }
+      var onCropStart = null;
+      if (typeof opts.onCropStart === 'function') {
+        onCropStart = opts.onCropStart;
+      }
+      var onCropEnd = null;
+      if (typeof opts.onCropEnd === 'function') {
+        onCropEnd = opts.onCropEnd;
+      }
+      var onCropMove = null;
+      if (typeof opts.onUpdate === 'function') {
+        console.warn('Croppr.js: `onUpdate` is deprecated and will be removed in the next major release. Please use `onCropMove` or `onCropEnd` instead.');
+        onCropMove = opts.onUpdate;
+      }
+      if (typeof opts.onCropMove === 'function') {
+        onCropMove = opts.onCropMove;
+      }
+      var returnMode = null;
+      if (opts.returnMode !== undefined) {
+        var s = opts.returnMode.toLowerCase();
+        if (['real', 'ratio', 'raw'].indexOf(s) === -1) {
+          throw "Invalid return mode.";
+        }
+        returnMode = s;
+      }
+      var convertToPixels = function convertToPixels(container) {
+        var width = container.offsetWidth;
+        var height = container.offsetHeight;
+        var sizeKeys = ['maxSize', 'minSize', 'startSize'];
+        for (var i = 0; i < sizeKeys.length; i++) {
+          var key = sizeKeys[i];
+          if (this[key] !== null) {
+            if (this[key].unit == '%') {
+              if (this[key].width !== null) {
+                this[key].width = this[key].width / 100 * width;
+              }
+              if (this[key].height !== null) {
+                this[key].height = this[key].height / 100 * height;
+              }
+            }
+            delete this[key].unit;
+          }
+        }
+      };
+      var defaultValue = function defaultValue(v, d) {
+        return v !== null ? v : d;
+      };
+      return {
+        aspectRatio: defaultValue(aspectRatio, defaults$$1.aspectRatio),
+        maxSize: defaultValue(maxSize, defaults$$1.maxSize),
+        minSize: defaultValue(minSize, defaults$$1.minSize),
+        startSize: defaultValue(startSize, defaults$$1.startSize),
+        returnMode: defaultValue(returnMode, defaults$$1.returnMode),
+        onInitialize: defaultValue(onInitialize, defaults$$1.onInitialize),
+        onCropStart: defaultValue(onCropStart, defaults$$1.onCropStart),
+        onCropMove: defaultValue(onCropMove, defaults$$1.onCropMove),
+        onCropEnd: defaultValue(onCropEnd, defaults$$1.onCropEnd),
+        convertToPixels: convertToPixels
+      };
+    }
+  }]);
+  return CropprCore;
+}();
+function round(value, decimals) {
+  return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
+}
+
+var Croppr$1 = function (_CropprCore) {
+  inherits(Croppr, _CropprCore);
+  /**
+   * @constructor
+   * Calls the CropprCore's constructor.
+   */
+  function Croppr(element, options) {
+    var _deferred = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+    classCallCheck(this, Croppr);
+    return possibleConstructorReturn(this, (Croppr.__proto__ || Object.getPrototypeOf(Croppr)).call(this, element, options, _deferred));
+  }
+  /**
+   * Gets the value of the crop region.
+   * @param {String} [mode] Which mode of calculation to use: 'real', 'ratio' or
+   *      'raw'.
+   */
+  createClass(Croppr, [{
+    key: 'getValue',
+    value: function getValue(mode) {
+      return get(Croppr.prototype.__proto__ || Object.getPrototypeOf(Croppr.prototype), 'getValue', this).call(this, mode);
+    }
+    /**
+     * Changes the image src.
+     * @param {String} src
+     */
+  }, {
+    key: 'setImage',
+    value: function setImage(src) {
+      return get(Croppr.prototype.__proto__ || Object.getPrototypeOf(Croppr.prototype), 'setImage', this).call(this, src);
+    }
+  }, {
+    key: 'destroy',
+    value: function destroy() {
+      return get(Croppr.prototype.__proto__ || Object.getPrototypeOf(Croppr.prototype), 'destroy', this).call(this);
+    }
+    /**
+     * Moves the crop region to a specified coordinate.
+     * @param {Number} x
+     * @param {Number} y
+     */
+  }, {
+    key: 'moveTo',
+    value: function moveTo(x, y) {
+      this.box.move(x, y);
+      this.redraw();
+      if (this.options.onCropEnd !== null) {
+        this.options.onCropEnd(this.getValue());
+      }
+      return this;
+    }
+    /**
+     * Resizes the crop region to a specified width and height.
+     * @param {Number} width
+     * @param {Number} height
+     * @param {Array} origin The origin point to resize from.
+     *      Defaults to [0.5, 0.5] (center).
+     */
+  }, {
+    key: 'resizeTo',
+    value: function resizeTo(width, height) {
+      var origin = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [.5, .5];
+      this.box.resize(width, height, origin);
+      this.redraw();
+      if (this.options.onCropEnd !== null) {
+        this.options.onCropEnd(this.getValue());
+      }
+      return this;
+    }
+    /**
+     * Scale the crop region by a factor.
+     * @param {Number} factor
+     * @param {Array} origin The origin point to resize from.
+     *      Defaults to [0.5, 0.5] (center).
+     */
+  }, {
+    key: 'scaleBy',
+    value: function scaleBy(factor) {
+      var origin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [.5, .5];
+      this.box.scale(factor, origin);
+      this.redraw();
+      if (this.options.onCropEnd !== null) {
+        this.options.onCropEnd(this.getValue());
+      }
+      return this;
+    }
+  }, {
+    key: 'reset',
+    value: function reset() {
+      this.box = this.initializeBox(this.options);
+      this.redraw();
+      if (this.options.onCropEnd !== null) {
+        this.options.onCropEnd(this.getValue());
+      }
+      return this;
+    }
+  }]);
+  return Croppr;
+}(CropprCore);
+
+return Croppr$1;
+
+})));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/croppr2.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,73 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			@import "croppr.css";
+
+			dialog[croppr] img {
+				width: 200px;
+			}
+			canvas[result] {
+				width: 200px;
+			}
+		</style>
+		<script src="croppr.js"></script>
+		<script src="/uploadcare/uploadcare.js"></script>
+		<script>
+			let croppr;
+			function start() {
+				let dialog = document.querySelector('dialog');
+				dialog.showModal();
+				let img = dialog.querySelector('img');
+				//img.src = 'https://ucarecdn.com/ede0040a-b577-4c2e-aeaf-b7f557ba2747/-/quality/smart/';
+				croppr = new Croppr(img, {
+					aspectRatio: 1,
+				});
+			}
+			function cancelCrop() {
+				document.querySelector('dialog[open]').close();
+				croppr.destroy();
+			}
+			function crop() {
+				let value = croppr.getValue();
+				console.log(value);
+				let width = value.width;
+				let height = value.height;
+				croppr.destroy();
+				let dialog = document.querySelector('dialog[croppr]');
+				let imgCroppr = dialog.querySelector('img');
+				console.log(imgCroppr);
+				//let canvas = document.createElement('canvas');
+				let canvas = document.querySelector('canvas[result]');
+				canvas.width = width;
+				canvas.height = height;
+				let ctx = canvas.getContext('2d');
+				ctx.drawImage(imgCroppr, value.x, value.y, width, height, 0, 0, width, height);
+				//let imgResult = document.querySelector('img[result]');
+				//imgResult.url = canvas.toDataURL();
+				dialog.close();
+			}
+			async function loaded(input) {
+				let info = { file: input.files[0] };
+				await uploadcare.infoAddUrl(info);
+				let img = document.querySelector('dialog[croppr] img');
+				img.src = info.url;
+				start();
+			}
+		</script>
+	</head>
+	<body>
+		<p><input type=file onchange="loaded(this)"></p>
+		<dialog croppr>
+			<p><img></p>
+			<p>
+				<button onclick="crop()">crop</button>
+				<button onclick="cancelCrop()">cancel</button>
+			</p>
+		</dialog>
+		<p>result</p>
+		<p><canvas result></canvas></p>
+		<p>bottom</p>
+	</body>
+</html>
Binary file src/uploadcare/processing.gif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/test1.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,42 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			@import "/uploadcare/croppr.css";
+			@import "/uploadcare/uploadcare.css";
+
+			img {
+				height: 100px;
+			}
+		</style>
+		<script src="/uploadcare/croppr.js"></script>
+		<script src="/uploadcare/uploadcare.js"></script>
+		<script>
+			'use strict';
+
+			uploadcare.publicKey = '718cc25ec1508ca5801d';
+			uploadcare.imagesOnly = true;
+			uploadcare.doNotStore = true;
+			uploadcare.maxFileSize = 10000000;
+			uploadcare.cropprOptions = {};
+
+			function uploaded(uuid,filename) {
+				let url = 'https://ucarecdn.com/' + uuid + '/-/quality/smart/';
+				let a = document.querySelector('a');
+				a.textContent = filename;
+				a.href = url;
+				document.querySelector('img').src = url;
+			}
+		</script>
+	</head>
+	<body>
+		<p>top</p>
+		<p>
+			<button onclick="uploadcare.upload(uploaded)">Upload File</button>
+		</p>
+		<p><a></a></p>
+		<p><img></p>
+		<p>bottom</p>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/test2.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,62 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local body_header = Shared.body_header or error()
+local footer = Shared.footer or error()
+
+
+return function()
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<style>
+			div[content] {
+				margin-left: 3%;
+				margin-right: 3%;
+			}
+
+			img {
+				height: 100px;
+			}
+		</style>
+		<script>
+			'use strict';
+
+			uploadcare.publicKey = '718cc25ec1508ca5801d';  // local
+			uploadcare.doNotStore = true;
+			uploadcare.cropprOptions = {};
+
+			function uploaded(uuid,filename) {
+				let url = 'https://ucarecdn.com/' + uuid + '/-/quality/smart/';
+				// console.log(url);
+				let a = document.querySelector('[content] a');
+				a.textContent = filename;
+				a.href = url;
+				document.querySelector('[content] img').src = url;
+			}
+		</script>
+	</head>
+	<body>
+	<div full>
+<%		body_header() %>
+		<div content>
+			<p>top</p>
+			<p>
+				<button onclick="uploadcare.upload(uploaded)">Upload File</button>
+			</p>
+			<p><a></a></p>
+			<p><img></p>
+			<p>bottom</p>
+		</div>
+<%		footer() %>
+	</div>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/thumbnails.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,77 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			@import "../tools/tools.css";
+
+			textarea {
+				width: 100%;
+				height: 100px;
+			}
+			p[img] span {
+				display: inline-block;
+			}
+			img {
+				width: 100%;
+				height: 100%;
+				object-fit: cover;
+			}
+		</style>
+		<style change></style>
+		<script src="/uploadcare/uploadcare.js"></script>
+		<script>
+			'use strict';
+
+			function show() {
+				let dim = document.querySelector('input').value;
+				let style = document.querySelector('style[change]');
+				style.innerHTML = `
+					p[img] span {
+						width: ${dim};
+						height: ${dim};
+					}
+`				;
+				let urls = document.querySelector('textarea').value;
+				urls = urls.split(/\s+/);
+				let htmlImg = '';
+				let htmlSizes = '';
+				for( let i=0; i<urls.length; i++ ) {
+					let url = urls[i];
+					if( url ) {
+						htmlImg += `<span><img src="${url}"></span> `;
+						htmlSizes += `<span s${i}></span> `;
+						let selector = `span[s${i}]`;
+						let request = new XMLHttpRequest();
+						request.open( 'HEAD', url );
+						request.onload = function() {
+							if( request.status === 200 ) {
+								let span = document.querySelector(selector);
+								span.textContent = request.getResponseHeader("Content-Length");
+							}
+						};
+						request.send();
+					}
+				}
+				document.querySelector('p[img]').innerHTML = htmlImg;
+				document.querySelector('span[sizes]').innerHTML = htmlSizes;
+			}
+		</script>
+	</head>
+	<body>
+		<p><a href="https://uploadcare.com/docs/transformations/image/">Uploadcare docs</a></p>
+		<p>
+			width/height:
+			<input value="300px">
+		</p>
+		<p>
+			URLs:<br>
+			<textarea></textarea>
+		</p>
+		<p>
+			<button onclick="show()">show</button>
+		</p>
+		<p img></p>
+		<p>Sizes: <span sizes></span></p>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/uploadcare.css	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,41 @@
+input[uploadcare][type="file"] {
+	display: none;
+}
+
+img[uploadcare] {
+	display: none;
+	z-index: 200;
+ 	box-sizing: border-box;
+	position: fixed;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%,-50%);
+	background-color: white;
+	width: 100px;
+	height: auto;
+	box-shadow: 2px 2px 10px 1px #8C8C8C;
+}
+
+dialog[croppr]::backdrop {
+	background-color: rgba(0,0,0,0.8);
+}
+dialog[croppr] img {
+	height: 400px;
+	max-height: calc(87vh - 80px);
+	xdisplay: block;
+}
+dialog[croppr] div[buttons] {
+	display: flex;
+	gap: 12px;
+	margin-top: 14px;
+}
+dialog[croppr] button {
+	padding: 5px 20px;
+	background-color: white;
+	border: 1px solid #4E4293;
+	color: #4E4293;
+}
+dialog[croppr] button:hover {
+	color: #9181EE;
+	border-color: #9181EE;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/uploadcare.js	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,267 @@
+'use strict';
+
+let uploadcare = {};
+
+/*
+
+to set an option:
+uploadcare[option] = value
+
+from https://uploadcare.com/docs/uploads/file-uploader-options/
+publicKey, imagesOnly, doNotStore
+
+onError - function called with request for AJAX errors
+maxFileSize - maximum file size of images in bytes
+
+*/
+
+uploadcare.maxDim = 2000;
+uploadcare.processingImage = '/uploadcare/processing.gif';
+uploadcare.cropprOptions = null;
+
+function logToServer(msg) {
+	ajax( '/log_info.js', 'msg='+encodeURIComponent(msg) );
+}
+
+{
+	// compression
+
+	function infoAddUrl(info) {
+		if( info.url )
+			return;
+		return new Promise( function(resolve) {
+			let reader = new FileReader();
+			reader.onload = function() {
+				info.url = reader.result;
+				resolve();
+			};
+			reader.readAsDataURL(info.file);
+		} );
+	}
+	uploadcare.infoAddUrl = infoAddUrl;
+
+	let croppr;
+	let dialog = null;
+
+	async function infoAddCroppedImage(info) {
+		if( !dialog ) {
+			let html = `
+				<dialog croppr>
+					<img>
+					<div buttons>
+						<button save>Save</button>
+						<button cancel>Cancel</button>
+					</div>
+				</dialog>
+`			;
+			document.body.insertAdjacentHTML( 'beforeend', html );
+			dialog = document.querySelector('dialog[croppr]');
+			dialog.onclose = function() {
+				croppr.destroy();
+			};
+		}
+		await infoAddUrl(info);
+		let image = dialog.querySelector('img');
+		image.src = info.url;
+		return new Promise( function(resolve) {
+			dialog.querySelector('button[save]').onclick = function() {
+				info.image = image;
+				info.crop = croppr.getValue();
+				dialog.close();
+				resolve();
+			};
+			dialog.querySelector('button[cancel]').onclick = function() {
+				info.canceled = true;
+				dialog.close();
+				resolve();
+			};
+			image.onload = function() {
+				dialog.showModal();
+				croppr = new Croppr( image, uploadcare.cropprOptions );
+			};
+		} );
+	}
+
+	let supportsDialog = typeof HTMLDialogElement === 'function';
+
+	async function infoAddImage(info) {
+		if( info.image )
+			return;
+		if( uploadcare.cropprOptions && supportsDialog )
+			return infoAddCroppedImage(info);
+		await infoAddUrl(info);
+		return new Promise( function(resolve) {
+			let image = new Image();
+			info.image = image;
+			image.src = info.url;
+			image.onload = function() {
+				resolve();
+			};
+		} );
+	}
+
+	async function infoAddCanvas(info,maxDim) {
+		await infoAddImage(info);
+		if( info.canceled )
+			return;
+		let image = info.image;
+		let width, height;
+		let crop = info.crop;
+		if( crop ) {
+			width = crop.width;
+			height = crop.height;
+		} else {
+			width = image.width;
+			height = image.height;
+		}
+		if( maxDim ) {
+			if( width > height ) {
+				if( width > maxDim ) {
+					height *= maxDim / width;
+					width = maxDim;
+				}
+			} else {
+				if( height > maxDim ) {
+					width *= maxDim / height;
+					height = maxDim;
+				}
+			}
+		}
+		let canvas = document.createElement('canvas');
+		canvas.width = width;
+		canvas.height = height;
+		let ctx = canvas.getContext('2d');
+		if( crop ) {
+			ctx.drawImage( image, crop.x, crop.y, crop.width, crop.height, 0, 0, width, height );
+		} else {
+			ctx.drawImage( image, 0, 0, width, height );
+		}
+		info.canvas = canvas;
+	}
+
+	async function infoAddBlob(info,maxDim) {
+		await infoAddCanvas(info,maxDim);
+		if( info.canceled )
+			return;
+		return new Promise( function(resolve) {
+			function done(blob) {
+				info.blob = blob;
+				resolve();
+			}
+			info.canvas.toBlob( done, 'image/jpeg' );
+		} );
+	}
+
+	async function infoCompress(info) {
+		let file = info.file;
+		await infoAddBlob(info);
+		if( info.canceled )
+			return;
+		let maxFileSize = uploadcare.maxFileSize;
+		if( !info.blob || maxFileSize && info.blob.size > maxFileSize ) {
+			await infoAddBlob(info,uploadcare.maxDim);
+			if( !info.blob )  throw 'no blob';
+		}
+		info.compressed = info.blob;
+		info.compressedName = file.name.replace( /(\.[^.]*)?$/, '.jpeg' );
+	}
+	uploadcare.infoCompress = infoCompress;
+
+
+	// uploading
+
+	function onload(url,onSuccess,onError,count) {
+		count = count || 1;
+		let request = new XMLHttpRequest();
+		request.open( 'GET', url );
+		request.onload = function() {
+			if( request.status === 200 ) {
+				onSuccess();
+			} else if( request.status === 404 ) {
+				console.log('onload failed '+count);
+				if( count >= 20 ) {
+					let text = 'Failed to get image after ' + count + ' tries, please try again';
+					onError( request.status, text );
+					return;
+				}
+				setTimeout( function() {
+					onload(url,onSuccess,onError,count+1);
+				}, 1000 );
+			} else {
+				onError( request.status, request.responseText) ;
+			}
+		};
+		request.send();
+	}
+
+	function call(file,filename,callback,onError) {
+		let request = new XMLHttpRequest();
+		let url = 'https://upload.uploadcare.com/base/';
+		request.open( 'POST', url );
+		request.onload = function() {
+			if( request.status !== 200 ) {
+				onError( request.status, request.responseText );
+				return;
+			}
+			let response = JSON.parse(request.responseText);
+			let uuid = response.file;
+			let url = 'https://ucarecdn.com/' + uuid + '/';
+			function onSuccess() {
+				callback( uuid, file.name );
+			}
+			onload( url, onSuccess, onError );
+		};
+		let formData = new FormData();
+		formData.append( 'UPLOADCARE_PUB_KEY', uploadcare.publicKey );
+		formData.append( 'UPLOADCARE_STORE', uploadcare.doNotStore ? '0' : '1' );
+		formData.append( 'file', file, filename );
+		request.send(formData);
+	}
+
+	let input = null;
+	let img = null;
+
+	function showImg() {
+		img.style.display = 'block';
+	}
+
+	function hideImg() {
+		img.style.display = 'none';
+	}
+
+	uploadcare.upload = function(callback) {
+		if( !uploadcare.publicKey )
+			throw new Error('uploadcare.publicKey required');
+		if( !input ) {
+			let html = `
+				<input uploadcare type=file>
+				<img uploadcare src="${uploadcare.processingImage}">
+`			;
+			document.body.insertAdjacentHTML( 'beforeend', html );
+			input = document.querySelector('input[uploadcare][type="file"]');
+			img = document.querySelector('img[uploadcare]');
+		}
+		input.accept = uploadcare.imagesOnly ? 'image/*' : '';
+		input.onchange = async function() {
+			function onError(status,text) {
+				hideImg();
+				if( text )
+					alert(text);
+				if( uploadcare.onError )
+					uploadcare.onError(status,text);
+			}
+			function callback2(uuid,filename) {
+				hideImg();
+				callback(uuid,filename)
+			}
+			let info = { file: input.files[0] };
+			input.value = null;
+			await infoCompress(info);
+			if( !info.canceled ) {
+				showImg();
+				call(info.compressed,info.compressedName,callback2,onError);
+			}
+		};
+		input.click();
+	};
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/uploader.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,27 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+		</style>
+		<script src="https://ucarecdn.com/libs/widget/3.x/uploadcare.full.min.js"></script>
+		<script>
+			'use strict';
+			var UPLOADCARE_PUBLIC_KEY = '718cc25ec1508ca5801d';
+			var UPLOADCARE_TABS = 'file camera url facebook gdrive gphotos instagram';
+			var UPLOADCARE_DO_NOT_STORE = true;
+			var UPLOADCARE_IMAGES_ONLY = true;
+		</script>
+	</head>
+	<body>
+		<p>top</p>
+		<p>
+			<input
+				type="hidden"
+				role="uploadcare-uploader"
+			/>
+		</p>
+		<p><a></a></p>
+		<p>bottom</p>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/uploadcare/uploader2.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,45 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+		</style>
+		<script src="https://ucarecdn.com/libs/widget/3.x/uploadcare.full.min.js"></script>
+		<script>
+			'use strict';
+			var UPLOADCARE_PUBLIC_KEY = '718cc25ec1508ca5801d';
+			var UPLOADCARE_TABS = 'file camera url facebook gdrive gphotos instagram';
+			var UPLOADCARE_DO_NOT_STORE = true;
+			var UPLOADCARE_IMAGES_ONLY = true;
+
+			let upload_result = null; /* For debug. */
+
+			function upload() {
+				// openDialog() apparently returns a promise, that, when done, returns another promise
+				// that, when done, returns object with file information. At least in case of success.
+				uploadcare.openDialog().done(
+					function( promise2 ) {
+						promise2.done(on_uploaded);
+					}
+				)
+			}
+
+			function on_uploaded( file ) {
+				upload_result = file;
+				if( file && file.cdnUrl ) {
+					let result = document.getElementById("result");
+					result.src = file.cdnUrl;
+				}
+			}
+		</script>
+	</head>
+	<body>
+		<p>top</p>
+		<p>
+			<button onclick="upload()">Upload</button></br>
+			<img id="result"></img>
+		</p>
+		<p><a></a></p>
+		<p>bottom</p>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/push.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -e
+
+hg identify >src/private/rev.txt
+
+PASSWORD=$(luan 'string:require("luan:Io.luan").stdout.write(require("file:../src/private/Config.luan").push_password)')
+
+luan luan:host/push.luan unsubscribe.linkmy.style $PASSWORD src 2>&1 | tee err
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/redir/push.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -e
+
+PASSWORD=$(luan 'string:require("luan:Io.luan").stdout.write(require("file:../../src/private/Config.luan").push_password)')
+
+luan luan:host/push.luan unsubscribe.linkmystyle.com $PASSWORD src 2>&1 | tee err
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/redir/serve.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+luan luan:http/serve.luan src 2>&1 | tee err
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/redir/src/index.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,3 @@
+local Http = require "luan:http/Http.luan"
+
+return Http.not_found_handler
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/redir/src/init.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,14 @@
+local Http = require "luan:http/Http.luan"
+local Hosted = require "luan:host/Hosted.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "init"
+
+
+Hosted.set_https and Hosted.set_https(Http.domain~=nil)
+
+function Http.not_found_handler()
+	Http.response.send_redirect("https://unsubscribe.linkmy.style"..Http.request.raw_path)
+	return true
+end
+
+return true
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/serve.sh	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+luan luan:http/serve.luan src 2>&1 | tee err
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/src/init.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,5 @@
+local Hosted = require "luan:host/Hosted.luan"
+
+Hosted.set_https and Hosted.set_https(true)
+
+return true
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/src/lib/Db.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,32 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local new_error = Luan.new_error or error()
+local Lucene = require "luan:lucene/Lucene.luan"
+local Io = require "luan:Io.luan"
+local uri = Io.uri or error()
+local Http = require "luan:http/Http.luan"
+local Thread = require "luan:Thread.luan"
+local Time = require "luan:Time.luan"
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "Db"
+
+
+local dir = uri("site:/private/local/lucene")
+
+local Db = Lucene.index( dir, {
+	log_dir = uri("site:/private/local/lucene_log")
+	name = "lucene"
+	version = 1
+} )
+
+Db.indexed_fields.unsubscribe_email = Lucene.type.lowercase
+
+function Db.not_in_transaction()
+	logger.error(new_error("not in transaction"))
+end
+
+if Http.is_serving then
+	Thread.schedule( Db.check, { delay=0, repeating_delay=Time.period{hours=1} } )
+end
+
+return Db
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/src/lib/Shared.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,50 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local String = require "luan:String.luan"
+local regex = String.regex or error()
+
+
+local Shared = {}
+
+function Shared.head()
+%>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			@import "/site.css";
+		</style>
+<%
+end
+
+function Shared.header()
+%>
+		<div header>
+			<a header href="https://linkmy.style/">
+				<img logo=big src="https://linkmy.style/images/logo.png">
+				<img logo=small src="https://linkmy.style/images/small_logo.png">
+			</a>
+		</div>
+<%
+end
+
+function Shared.footer()
+%>
+		<div footer>
+			<span>
+				<a href="https://linkmy.style/help.html">Help</a>
+				<br>support@linkmy.style
+			</span>
+			<span>
+				<a href="https://apps.apple.com/us/app/linkmystyle/id6475133762"><img ios src="https://linkmy.style/images/ios.svg"></a>
+				<a href="https://www.instagram.com/linkmy.style/"><img src="https://linkmy.style/images/icons/instagram.svg"></a>
+			</span>
+		</div>
+<%
+end
+
+local email_regex = regex[[^[-+~.\w]+@[-.\w]+$]]
+
+function Shared.validate_email(email)
+	email_regex.matches(email) or error("bad email: "..email)
+end
+
+return Shared
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/src/private/tools/lucene.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,6 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Web_search = require "luan:lucene/Web_search.luan"
+local Db = require "site:/lib/Db.luan"
+
+return Web_search.of(Db)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/src/private/tools/tools.css	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,15 @@
+* {
+	box-sizing: border-box;
+}
+
+body {
+	font-family: Sans-Serif;
+	margin: 3%;
+}
+
+a {
+	text-decoration: none;
+}
+a:hover {
+	text-decoration: underline;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/src/private/tools/tools.html	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,20 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			@import "tools.css";
+		</style>
+	</head>
+	<body>
+		<h1>Unsubscribe Tools</h1>
+		<p>
+			https://unsubscribe.linkmystyle.com/unsubscribe.html?email=someone@somewhere.com
+			<br>change the email
+		</p>
+		<p><a href="unsubscribed.html">unsubscribed</a></p>
+		<p><a href="../local/logs/">logs</a></p>
+		<p><a href="../rev.txt">hg rev</a></p>
+		<p><a href="lucene.html">lucene</a></p>
+	</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/src/private/tools/unsubscribed.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,41 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Db = require "site:/lib/Db.luan"
+
+
+return function()
+	local docs = Db.search("type:unsubscribe",1,1000000)
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<style>
+			@import "tools.css";
+
+			body {
+				margin-top: 0;
+				margin-bottom: 0;
+			}
+		</style>
+	</head>
+	<body>
+<%
+	for _, doc in ipairs(docs) do
+		local email = doc.unsubscribe_email or error()
+%>
+		<p>
+			<a href="/subscribe.html?email=<%= email %>">X</a>
+			<%= email %>
+		</p>
+<%
+	end
+%>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/src/site.css	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,62 @@
+* {
+	box-sizing: border-box;
+}
+
+body {
+	font-family: Bitter;
+	margin: 0;
+}
+
+a {
+	text-decoration: none;
+}
+a:hover {
+	text-decoration: underline;
+}
+
+div[content] {
+	margin-left: 40px;
+	margin-right: 40px;
+	text-align: center;
+}
+
+div[header] {
+	display: flex;
+	padding: 20px 40px;
+	justify-content: space-between;
+	align-items: center;
+	background-color: #DBD5FF;
+}
+img[logo] {
+	height: 50px;
+	display: block;
+}
+@media (min-width: 757px) {
+	img[logo="small"] {
+		display: none;
+	}
+}
+@media (max-width: 756px) {
+	img[logo="big"] {
+		display: none;
+	}
+}
+
+div[footer] {
+	padding: 20px 40px;
+	background-color: #DBD5FF;
+	color: #4E4293;
+	position: absolute;
+	bottom: 0;
+	width: 100%;
+	display: flex;
+	justify-content: space-between;
+}
+div[footer] img {
+	height: 40px;
+	display: inline-block;
+}
+div[footer] img[ios] {
+	padding-top: 4px;
+	padding-bottom: 4px;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/src/subscribe.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,38 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local header = Shared.header or error()
+local footer = Shared.footer or error()
+local validate_email = Shared.validate_email or error()
+local Db = require "site:/lib/Db.luan"
+
+
+return function()
+	local email = Http.request.parameters.email or error()
+	validate_email(email)
+	Db.run_in_transaction( function()
+		Db.delete("unsubscribe_email:"..email)
+	end )
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+	</head>
+	<body>
+<%		header() %>
+		<div content>
+			<h1>Subscribed</h1>
+			<p><%=email%> has been subscribed to linkmy.style .</p>
+			<p><a href="unsubscribe.html?email=<%=email%>">Unsubscribe</a></p>
+		</div>
+<%		footer() %>
+	</body>
+</html>
+<%
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/unsubscribe/src/unsubscribe.html.luan	Fri Jul 11 20:57:49 2025 -0600
@@ -0,0 +1,42 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Shared = require "site:/lib/Shared.luan"
+local head = Shared.head or error()
+local header = Shared.header or error()
+local footer = Shared.footer or error()
+local validate_email = Shared.validate_email or error()
+local Db = require "site:/lib/Db.luan"
+
+
+return function()
+	local email = Http.request.parameters.email or error()
+	validate_email(email)
+	Db.run_in_transaction( function()
+		Db.delete("unsubscribe_email:"..email)
+		Db.save{
+			type = "unsubscribe"
+			unsubscribe_email = email
+		}
+	end )
+	Io.stdout = Http.response.text_writer()
+%>
+<!doctype html>
+<html lang="en">
+	<head>
+<%		head() %>
+		<title>Link My Style</title>
+	</head>
+	<body>
+<%		header() %>
+		<div content>
+			<h1>Unsubscribed</h1>
+			<p><%=email%> has been unsubscribed from linkmy.style .</p>
+			<p>Unsubscribed by mistake? <a href="subscribe.html?email=<%=email%>">Subscribe</a></p>
+		</div>
+<%		footer() %>
+	</body>
+</html>
+<%
+end