diff --git a/onlylegs/__init__.py b/onlylegs/__init__.py
deleted file mode 100644
index e15e3e1..0000000
--- a/onlylegs/__init__.py
+++ /dev/null
@@ -1,136 +0,0 @@
-"""
-Onlylegs Gallery
-This is the main app file, it loads all the other files and sets up the app
-"""
-import os
-import logging
-
-from flask_assets import Bundle
-from flask_migrate import init as migrate_init
-
-from flask import Flask, render_template, abort
-from werkzeug.exceptions import HTTPException
-from werkzeug.security import generate_password_hash
-
-from onlylegs.extensions import db, migrate, login_manager, assets, compress, cache
-from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR
-from onlylegs.models import User
-from onlylegs.views import (
-    index as view_index,
-    image as view_image,
-    group as view_group,
-    settings as view_settings,
-    profile as view_profile,
-)
-from onlylegs.api import media as api_media, group as api_group, account as api_account
-from onlylegs import auth as view_auth
-from onlylegs import gwagwa
-
-
-def create_app():  # pylint: disable=R0914
-    """
-    Create and configure the main app
-    """
-    app = Flask(__name__, instance_path=INSTANCE_DIR)
-    app.config.from_pyfile("config.py")
-
-    # DATABASE
-    db.init_app(app)
-    migrate.init_app(app, db, directory=MIGRATIONS_DIR)
-
-    # If database file doesn't exist, create it
-    if not os.path.exists(os.path.join(INSTANCE_DIR, "gallery.sqlite3")):
-        print("Creating database")
-        with app.app_context():
-            db.create_all()
-
-            register_user = User(
-                username=app.config["ADMIN_CONF"]["username"],
-                email=app.config["ADMIN_CONF"]["email"],
-                password=generate_password_hash("changeme!", method="sha256"),
-            )
-            db.session.add(register_user)
-            db.session.commit()
-
-            print(
-                """
-####################################################
-# DEFAULY ADMIN USER GENERATED WITH GIVEN USERNAME #
-# THE DEFAULT PASSWORD "changeme!" HAS BEEN USED,  #
-# PLEASE UPDATE IT IN THE SETTINGS!                #
-####################################################
-            """
-            )
-
-    # Check if migrations directory exists, if not create it
-    with app.app_context():
-        if not os.path.exists(MIGRATIONS_DIR):
-            print("Creating migrations directory")
-            migrate_init(directory=MIGRATIONS_DIR)
-
-    # LOGIN MANAGER
-    # can also set session_protection to "strong"
-    # this would protect against session hijacking
-    login_manager.init_app(app)
-    login_manager.login_view = "onlylegs.index"
-
-    @login_manager.user_loader
-    def load_user(user_id):  # skipcq: PTC-W0065
-        return User.query.filter_by(alt_id=user_id).first()
-
-    @login_manager.unauthorized_handler
-    def unauthorized():  # skipcq: PTC-W0065
-        error = 401
-        msg = "You are not authorized to view this page!!!!"
-        return render_template("error.html", error=error, msg=msg), error
-
-    # ERROR HANDLERS
-    @app.errorhandler(Exception)
-    def error_page(err):  # skipcq: PTC-W0065
-        """
-        Error handlers, if the error is not a HTTP error, return 500
-        """
-        if not isinstance(err, HTTPException):
-            abort(500)
-        return (
-            render_template("error.html", error=err.code, msg=err.description),
-            err.code,
-        )
-
-    # ASSETS
-    assets.init_app(app)
-
-    scripts = Bundle(
-        "js/*.js", output="gen/js.js", depends="js/*.js"
-    )  # filter jsmin is broken :c
-    styles = Bundle(
-        "sass/style.sass",
-        filters="libsass, cssmin",
-        output="gen/styles.css",
-        depends="sass/**/*.sass",
-    )
-
-    assets.register("scripts", scripts)
-    assets.register("styles", styles)
-
-    # BLUEPRINTS
-    app.register_blueprint(view_auth.blueprint)
-    app.register_blueprint(view_index.blueprint)
-    app.register_blueprint(view_image.blueprint)
-    app.register_blueprint(view_group.blueprint)
-    app.register_blueprint(view_profile.blueprint)
-    app.register_blueprint(view_settings.blueprint)
-
-    # APIS
-    app.register_blueprint(api_media.blueprint)
-    app.register_blueprint(api_group.blueprint)
-    app.register_blueprint(api_account.blueprint)
-
-    # CACHE AND COMPRESS
-    cache.init_app(app)
-    compress.init_app(app)
-
-    # Yupee! We got there :3
-    print("Done!")
-    logging.info("Gallery started successfully!")
-    return app
diff --git a/onlylegs/api.py b/onlylegs/api.py
new file mode 100644
index 0000000..fffeea5
--- /dev/null
+++ b/onlylegs/api.py
@@ -0,0 +1,182 @@
+"""
+Onlylegs - API endpoints
+"""
+import os
+import pathlib
+import re
+import logging
+from uuid import uuid4
+
+from flask import (
+    Blueprint,
+    abort,
+    send_from_directory,
+    jsonify,
+    request,
+    current_app,
+)
+from flask_login import login_required, current_user
+
+from colorthief import ColorThief
+
+from onlylegs.extensions import db
+from onlylegs.models import Users, Pictures
+from onlylegs.utils.metadata import yoink
+from onlylegs.utils.generate_image import generate_thumbnail
+
+
+blueprint = Blueprint("api", __name__, url_prefix="/api")
+
+
+@blueprint.route("/account/picture/<int:user_id>", methods=["POST"])
+@login_required
+def account_picture(user_id):
+    """
+    Returns the profile of a user
+    """
+    user = db.get_or_404(Users, user_id)
+    file = request.files.get("file", None)
+
+    # If no image is uploaded, return 404 error
+    if not file:
+        return jsonify({"error": "No file uploaded"}), 400
+    if user.id != current_user.id:
+        return jsonify({"error": "You are not allowed to do this, go away"}), 403
+
+    # Get file extension, generate random name and set file path
+    img_ext = pathlib.Path(file.filename).suffix.replace(".", "").lower()
+    img_name = str(user.id)
+    img_path = os.path.join(current_app.config["PFP_FOLDER"], img_name + "." + img_ext)
+
+    # Check if file extension is allowed
+    if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys():
+        logging.info("File extension not allowed: %s", img_ext)
+        return jsonify({"error": "File extension not allowed"}), 403
+
+    if user.picture:
+        # Delete cached files and old image
+        os.remove(os.path.join(current_app.config["PFP_FOLDER"], user.picture))
+        cache_name = user.picture.rsplit(".")[0]
+        for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
+            cache_name + "*"
+        ):
+            os.remove(cache_file)
+
+    # Save file
+    try:
+        file.save(img_path)
+    except OSError as err:
+        logging.info("Error saving file %s because of %s", img_path, err)
+        return jsonify({"error": "Error saving file"}), 500
+
+    img_colors = ColorThief(img_path).get_color()
+
+    # Save to database
+    user.colour = img_colors
+    user.picture = str(img_name + "." + img_ext)
+    db.session.commit()
+
+    return jsonify({"message": "File uploaded"}), 200
+
+
+@blueprint.route("/account/username/<int:user_id>", methods=["POST"])
+@login_required
+def account_username(user_id):
+    """
+    Returns the profile of a user
+    """
+    user = db.get_or_404(Users, user_id)
+    new_name = request.form["name"]
+
+    username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b")
+
+    # Validate the form
+    if not new_name or not username_regex.match(new_name):
+        return jsonify({"error": "Username is invalid"}), 400
+    if user.id != current_user.id:
+        return jsonify({"error": "You are not allowed to do this, go away"}), 403
+
+    # Save to database
+    user.username = new_name
+    db.session.commit()
+
+    return jsonify({"message": "Username changed"}), 200
+
+
+@blueprint.route("/media/<path:path>", methods=["GET"])
+def media(path):
+    """
+    Returns image from media folder
+    r for resolution, thumb for thumbnail etc
+    e for extension, jpg, png etc
+    """
+    res = request.args.get("r", "").strip()
+    ext = request.args.get("e", "").strip()
+
+    # if no args are passed, return the raw file
+    if not res and not ext:
+        if not os.path.exists(os.path.join(current_app.config["MEDIA_FOLDER"], path)):
+            abort(404)
+        return send_from_directory(current_app.config["MEDIA_FOLDER"], path)
+
+    # Generate thumbnail, if None is returned a server error occured
+    thumb = generate_thumbnail(path, res, ext)
+    if not thumb:
+        abort(500)
+
+    response = send_from_directory(os.path.dirname(thumb), os.path.basename(thumb))
+    response.headers["Cache-Control"] = "public, max-age=31536000"
+    response.headers["Expires"] = "31536000"
+
+    return response
+
+
+@blueprint.route("/media/upload", methods=["POST"])
+@login_required
+def upload():
+    """
+    Uploads an image to the server and saves it to the database
+    """
+    form_file = request.files.get("file", None)
+    form = request.form
+
+    if not form_file:
+        return jsonify({"message": "No file"}), 400
+
+    # Get file extension, generate random name and set file path
+    img_ext = pathlib.Path(form_file.filename).suffix.replace(".", "").lower()
+    img_name = "GWAGWA_" + str(uuid4())
+    img_path = os.path.join(
+        current_app.config["UPLOAD_FOLDER"], img_name + "." + img_ext
+    )
+
+    # Check if file extension is allowed
+    if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys():
+        logging.info("File extension not allowed: %s", img_ext)
+        return jsonify({"message": "File extension not allowed"}), 403
+
+    # Save file
+    try:
+        form_file.save(img_path)
+    except OSError as err:
+        logging.info("Error saving file %s because of %s", img_path, err)
+        return jsonify({"message": "Error saving file"}), 500
+
+    img_exif = yoink(img_path)  # Get EXIF data
+    img_colors = ColorThief(img_path).get_palette(color_count=3)  # Get color palette
+
+    # Save to database
+    query = Pictures(
+        author_id=current_user.id,
+        filename=img_name + "." + img_ext,
+        mimetype=img_ext,
+        exif=img_exif,
+        colours=img_colors,
+        description=form["description"],
+        alt=form["alt"],
+    )
+
+    db.session.add(query)
+    db.session.commit()
+
+    return jsonify({"message": "File uploaded"}), 200
diff --git a/onlylegs/api/__init__.py b/onlylegs/api/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/onlylegs/api/account.py b/onlylegs/api/account.py
deleted file mode 100644
index fb54c25..0000000
--- a/onlylegs/api/account.py
+++ /dev/null
@@ -1,93 +0,0 @@
-"""
-Onlylegs - API endpoints
-"""
-import os
-import pathlib
-import re
-import logging
-
-from flask import Blueprint, jsonify, request, current_app
-from flask_login import login_required, current_user
-
-from colorthief import ColorThief
-
-from onlylegs.extensions import db
-from onlylegs.models import User
-
-
-blueprint = Blueprint("account_api", __name__, url_prefix="/api/account")
-
-
-@blueprint.route("/picture/<int:user_id>", methods=["POST"])
-@login_required
-def account_picture(user_id):
-    """
-    Returns the profile of a user
-    """
-    user = db.get_or_404(User, user_id)
-    file = request.files["file"]
-
-    # If no image is uploaded, return 404 error
-    if not file:
-        return jsonify({"error": "No file uploaded"}), 400
-    if user.id != current_user.id:
-        return jsonify({"error": "You are not allowed to do this, go away"}), 403
-
-    # Get file extension, generate random name and set file path
-    img_ext = pathlib.Path(file.filename).suffix.replace(".", "").lower()
-    img_name = str(user.id)
-    img_path = os.path.join(current_app.config["PFP_FOLDER"], img_name + "." + img_ext)
-
-    # Check if file extension is allowed
-    if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys():
-        logging.info("File extension not allowed: %s", img_ext)
-        return jsonify({"error": "File extension not allowed"}), 403
-
-    if user.picture:
-        # Delete cached files and old image
-        os.remove(os.path.join(current_app.config["PFP_FOLDER"], user.picture))
-        cache_name = user.picture.rsplit(".")[0]
-        for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
-            cache_name + "*"
-        ):
-            os.remove(cache_file)
-
-    # Save file
-    try:
-        file.save(img_path)
-    except OSError as err:
-        logging.info("Error saving file %s because of %s", img_path, err)
-        return jsonify({"error": "Error saving file"}), 500
-
-    img_colors = ColorThief(img_path).get_color()
-
-    # Save to database
-    user.colour = img_colors
-    user.picture = str(img_name + "." + img_ext)
-    db.session.commit()
-
-    return jsonify({"message": "File uploaded"}), 200
-
-
-@blueprint.route("/username/<int:user_id>", methods=["POST"])
-@login_required
-def account_username(user_id):
-    """
-    Returns the profile of a user
-    """
-    user = db.get_or_404(User, user_id)
-    new_name = request.form["name"]
-
-    username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b")
-
-    # Validate the form
-    if not new_name or not username_regex.match(new_name):
-        return jsonify({"error": "Username is invalid"}), 400
-    if user.id != current_user.id:
-        return jsonify({"error": "You are not allowed to do this, go away"}), 403
-
-    # Save to database
-    user.username = new_name
-    db.session.commit()
-
-    return jsonify({"message": "Username changed"}), 200
diff --git a/onlylegs/api/group.py b/onlylegs/api/group.py
deleted file mode 100644
index 9be39a5..0000000
--- a/onlylegs/api/group.py
+++ /dev/null
@@ -1,78 +0,0 @@
-"""
-Onlylegs - API endpoints
-"""
-from flask import Blueprint, flash, jsonify, request
-from flask_login import login_required, current_user
-
-from onlylegs.extensions import db
-from onlylegs.models import Post, Group, GroupJunction
-
-
-blueprint = Blueprint("group_api", __name__, url_prefix="/api/group")
-
-
-@blueprint.route("/create", methods=["POST"])
-@login_required
-def create_group():
-    """
-    Creates a group
-    """
-    new_group = Group(
-        name=request.form["name"],
-        description=request.form["description"],
-        author_id=current_user.id,
-    )
-
-    db.session.add(new_group)
-    db.session.commit()
-
-    return jsonify({"message": "Group created", "id": new_group.id})
-
-
-@blueprint.route("/modify", methods=["POST"])
-@login_required
-def modify_group():
-    """
-    Changes the images in a group
-    """
-    group_id = request.form["group"]
-    image_id = request.form["image"]
-    action = request.form["action"]
-
-    group = db.get_or_404(Group, group_id)
-    db.get_or_404(Post, image_id)  # Check if image exists
-
-    if group.author_id != current_user.id:
-        return jsonify({"message": "You are not the owner of this group"}), 403
-
-    if (
-        action == "add"
-        and not GroupJunction.query.filter_by(
-            group_id=group_id, post_id=image_id
-        ).first()
-    ):
-        db.session.add(GroupJunction(group_id=group_id, post_id=image_id))
-    elif request.form["action"] == "remove":
-        GroupJunction.query.filter_by(group_id=group_id, post_id=image_id).delete()
-
-    db.session.commit()
-    return jsonify({"message": "Group modified"})
-
-
-@blueprint.route("/delete", methods=["POST"])
-def delete_group():
-    """
-    Deletes a group
-    """
-    group_id = request.form["group"]
-    group = db.get_or_404(Group, group_id)
-
-    if group.author_id != current_user.id:
-        return jsonify({"message": "You are not the owner of this group"}), 403
-
-    GroupJunction.query.filter_by(group_id=group_id).delete()
-    db.session.delete(group)
-    db.session.commit()
-
-    flash(["Group yeeted!", "1"])
-    return jsonify({"message": "Group deleted"})
diff --git a/onlylegs/api/media.py b/onlylegs/api/media.py
deleted file mode 100644
index 2ce774b..0000000
--- a/onlylegs/api/media.py
+++ /dev/null
@@ -1,144 +0,0 @@
-"""
-Onlylegs - API endpoints
-Media upload and retrieval
-"""
-from uuid import uuid4
-import os
-import pathlib
-import logging
-
-from flask import (
-    Blueprint,
-    flash,
-    abort,
-    send_from_directory,
-    jsonify,
-    request,
-    current_app,
-)
-from flask_login import login_required, current_user
-
-from colorthief import ColorThief
-
-from onlylegs.extensions import db
-from onlylegs.models import Post, GroupJunction
-from onlylegs.utils import metadata as mt
-from onlylegs.utils.generate_image import generate_thumbnail
-
-
-blueprint = Blueprint("media_api", __name__, url_prefix="/api/media")
-
-
-@blueprint.route("/<path:path>", methods=["GET"])
-def media(path):
-    """
-    Returns a file from the uploads folder
-    r for resolution, thumb for thumbnail etc
-    e for extension, jpg, png etc
-    """
-    res = request.args.get("r", default=None, type=str)
-    ext = request.args.get("e", default=None, type=str)
-    # path = secure_filename(path)
-
-    # if no args are passed, return the raw file
-    if not res and not ext:
-        if not os.path.exists(os.path.join(current_app.config["MEDIA_FOLDER"], path)):
-            abort(404)
-        return send_from_directory(current_app.config["MEDIA_FOLDER"], path)
-
-    thumb = generate_thumbnail(path, res, ext)
-    if not thumb:
-        abort(500)
-
-    return send_from_directory(os.path.dirname(thumb), os.path.basename(thumb))
-
-
-@blueprint.route("/upload", methods=["POST"])
-@login_required
-def upload():
-    """
-    Uploads an image to the server and saves it to the database
-    """
-    form_file = request.files["file"]
-    form = request.form
-
-    if not form_file:
-        return jsonify({"message": "No file"}), 400
-
-    # Get file extension, generate random name and set file path
-    img_ext = pathlib.Path(form_file.filename).suffix.replace(".", "").lower()
-    img_name = "GWAGWA_" + str(uuid4())
-    img_path = os.path.join(
-        current_app.config["UPLOAD_FOLDER"], img_name + "." + img_ext
-    )
-
-    # Check if file extension is allowed
-    if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys():
-        logging.info("File extension not allowed: %s", img_ext)
-        return jsonify({"message": "File extension not allowed"}), 403
-
-    # Save file
-    try:
-        form_file.save(img_path)
-    except OSError as err:
-        logging.info("Error saving file %s because of %s", img_path, err)
-        return jsonify({"message": "Error saving file"}), 500
-
-    img_exif = mt.Metadata(img_path).yoink()  # Get EXIF data
-    img_colors = ColorThief(img_path).get_palette(color_count=3)  # Get color palette
-
-    # Save to database
-    query = Post(
-        author_id=current_user.id,
-        filename=img_name + "." + img_ext,
-        mimetype=img_ext,
-        exif=img_exif,
-        colours=img_colors,
-        description=form["description"],
-        alt=form["alt"],
-    )
-
-    db.session.add(query)
-    db.session.commit()
-
-    return jsonify({"message": "File uploaded"}), 200
-
-
-@blueprint.route("/delete/<int:image_id>", methods=["POST"])
-@login_required
-def delete_image(image_id):
-    """
-    Deletes an image from the server and database
-    """
-    post = db.get_or_404(Post, image_id)
-
-    # Check if image exists and if user is allowed to delete it (author)
-    if post.author_id != current_user.id:
-        logging.info("User %s tried to delete image %s", current_user.id, image_id)
-        return (
-            jsonify({"message": "You are not allowed to delete this image, heck off"}),
-            403,
-        )
-
-    # Delete file
-    try:
-        os.remove(os.path.join(current_app.config["UPLOAD_FOLDER"], post.filename))
-    except FileNotFoundError:
-        logging.warning(
-            "File not found: %s, already deleted or never existed", post.filename
-        )
-
-    # Delete cached files
-    cache_name = post.filename.rsplit(".")[0]
-    for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
-        cache_name + "*"
-    ):
-        os.remove(cache_file)
-
-    GroupJunction.query.filter_by(post_id=image_id).delete()
-    db.session.delete(post)
-    db.session.commit()
-
-    logging.info("Removed image (%s) %s", image_id, post.filename)
-    flash(["Image was all in Le Head!", "1"])
-    return jsonify({"message": "Image deleted"}), 200
diff --git a/onlylegs/app.py b/onlylegs/app.py
new file mode 100644
index 0000000..0dd4349
--- /dev/null
+++ b/onlylegs/app.py
@@ -0,0 +1,138 @@
+"""
+Onlylegs Gallery
+This is the main app file, it loads all the other files and sets up the app
+"""
+import os
+import logging
+
+from flask_assets import Bundle
+from flask_migrate import init as migrate_init
+
+from flask import Flask, render_template, abort
+from werkzeug.exceptions import HTTPException
+from werkzeug.security import generate_password_hash
+
+from onlylegs.extensions import db, migrate, login_manager, assets, compress, cache
+from onlylegs.config import INSTANCE_DIR, MIGRATIONS_DIR
+from onlylegs.models import Users
+from onlylegs.views import (
+    index as view_index,
+    image as view_image,
+    group as view_group,
+    settings as view_settings,
+    profile as view_profile,
+)
+from onlylegs import api
+from onlylegs import auth as view_auth
+from onlylegs import filters
+
+app = Flask(__name__, instance_path=INSTANCE_DIR)
+app.config.from_pyfile("config.py")
+
+# DATABASE
+db.init_app(app)
+migrate.init_app(app, db, directory=MIGRATIONS_DIR)
+
+# If database file doesn't exist, create it
+if not os.path.exists(os.path.join(INSTANCE_DIR, "gallery.sqlite3")):
+    print("Creating database")
+    with app.app_context():
+        db.create_all()
+
+        register_user = Users(
+            username=app.config["ADMIN_CONF"]["username"],
+            email=app.config["ADMIN_CONF"]["email"],
+            password=generate_password_hash("changeme!", method="sha256"),
+        )
+        db.session.add(register_user)
+        db.session.commit()
+
+        print(
+            """
+####################################################
+# DEFAULT ADMIN USER GENERATED WITH GIVEN USERNAME #
+# THE DEFAULT PASSWORD "changeme!" HAS BEEN USED,  #
+# PLEASE UPDATE IT IN THE SETTINGS!                #
+####################################################
+        """
+        )
+
+# Check if migrations directory exists, if not create it
+with app.app_context():
+    if not os.path.exists(MIGRATIONS_DIR):
+        print("Creating migrations directory")
+        migrate_init(directory=MIGRATIONS_DIR)
+
+# LOGIN MANAGER
+# can also set session_protection to "strong"
+# this would protect against session hijacking
+login_manager.init_app(app)
+login_manager.login_view = "onlylegs.index"
+
+
+@login_manager.user_loader
+def load_user(user_id):
+    return Users.query.filter_by(alt_id=user_id).first()
+
+
+@login_manager.unauthorized_handler
+def unauthorized():
+    error = 401
+    msg = "You are not authorized to view this page!!!!"
+    return render_template("error.html", error=error, msg=msg), error
+
+
+# ERROR HANDLERS
+@app.errorhandler(Exception)
+def error_page(err):
+    """
+    Error handlers, if the error is not a HTTP error, return 500
+    """
+    if not isinstance(err, HTTPException):
+        abort(500)
+    return (
+        render_template("error.html", error=err.code, msg=err.description),
+        err.code,
+    )
+
+
+# ASSETS
+assets.init_app(app)
+
+scripts = Bundle(
+    "js/*.js", output="gen/js.js", depends="js/*.js"
+)  # filter jsmin is broken :c
+styles = Bundle(
+    "sass/style.sass",
+    filters="libsass, cssmin",
+    output="gen/styles.css",
+    depends="sass/**/*.sass",
+)
+
+assets.register("scripts", scripts)
+assets.register("styles", styles)
+
+# BLUEPRINTS
+app.register_blueprint(view_auth.blueprint)
+app.register_blueprint(view_index.blueprint)
+app.register_blueprint(view_image.blueprint)
+app.register_blueprint(view_group.blueprint)
+app.register_blueprint(view_profile.blueprint)
+app.register_blueprint(view_settings.blueprint)
+
+app.register_blueprint(api.blueprint)
+
+# FILTERS
+app.register_blueprint(filters.blueprint)
+
+# CACHE AND COMPRESS
+cache.init_app(app)
+compress.init_app(app)
+
+# Yupee! We got there :3
+print("Done!")
+logging.info("Gallery started successfully!")
+
+
+if __name__ == "__main__":
+    app.run()
diff --git a/onlylegs/auth.py b/onlylegs/auth.py
index 6b5884f..c3d0698 100644
--- a/onlylegs/auth.py
+++ b/onlylegs/auth.py
@@ -11,7 +11,7 @@ from werkzeug.security import check_password_hash, generate_password_hash
 from flask_login import login_user, logout_user, login_required
 
 from onlylegs.extensions import db
-from onlylegs.models import User
+from onlylegs.models import Users
 
 
 blueprint = Blueprint("auth", __name__, url_prefix="/auth")
@@ -28,7 +28,7 @@ def login():
     password = request.form["password"].strip()
     remember = bool(request.form["remember-me"])
 
-    user = User.query.filter_by(username=username).first()
+    user = Users.query.filter_by(username=username).first()
 
     if not user or not check_password_hash(user.password, password):
         logging.error("Login attempt from %s", request.remote_addr)
@@ -77,7 +77,7 @@ def register():
     elif password_repeat != password:
         error.append("Passwords do not match!")
 
-    user_exists = User.query.filter_by(username=username).first()
+    user_exists = Users.query.filter_by(username=username).first()
     if user_exists:
         error.append("User already exists!")
 
@@ -86,7 +86,7 @@ def register():
         print(error)
         return jsonify(error), 400
 
-    register_user = User(
+    register_user = Users(
         username=username,
         email=email,
         password=generate_password_hash(password, method="sha256"),
diff --git a/onlylegs/config.py b/onlylegs/config.py
index 58e7a80..06cd271 100644
--- a/onlylegs/config.py
+++ b/onlylegs/config.py
@@ -3,6 +3,7 @@ Gallery configuration file
 """
 import os
 import platformdirs
+import importlib.metadata
 from dotenv import load_dotenv
 from yaml import safe_load
 
@@ -41,3 +42,6 @@ MEDIA_FOLDER = os.path.join(user_dir, "media")
 # Database
 INSTANCE_DIR = instance_dir
 MIGRATIONS_DIR = os.path.join(INSTANCE_DIR, "migrations")
+
+# App
+APP_VERSION = importlib.metadata.version("OnlyLegs")
diff --git a/onlylegs/extensions.py b/onlylegs/extensions.py
index 0b27ca4..9e3eb84 100644
--- a/onlylegs/extensions.py
+++ b/onlylegs/extensions.py
@@ -13,4 +13,4 @@ migrate = Migrate()
 login_manager = LoginManager()
 assets = Environment()
 compress = Compress()
-cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 300})
+cache = Cache(config={"CACHE_TYPE": "simple", "CACHE_DEFAULT_TIMEOUT": 300})
diff --git a/onlylegs/filters.py b/onlylegs/filters.py
new file mode 100644
index 0000000..765003b
--- /dev/null
+++ b/onlylegs/filters.py
@@ -0,0 +1,20 @@
+"""
+OnlyLegs filters
+Custom Jinja2 filters
+"""
+from flask import Blueprint
+from onlylegs.utils import colour as colour_utils
+
+
+blueprint = Blueprint("filters", __name__)
+
+
+@blueprint.app_template_filter()
+def colour_contrast(colour):
+    """
+    Pass in the colour of the background and will return
+    a css variable based on the contrast of text required to be readable
+    "color: var(--fg-white);" or "color: var(--fg-black);"
+    """
+    colour_obj = colour_utils.Colour(colour)
+    return "rgb(var(--fg-black));" if colour_obj.is_light() else "rgb(var(--fg-white));"
diff --git a/onlylegs/gwagwa.py b/onlylegs/gwagwa.py
deleted file mode 100644
index 6fb58f6..0000000
--- a/onlylegs/gwagwa.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""
-Gwa Gwa!
-"""
-print("Gwa Gwa!")
diff --git a/onlylegs/langs/gb.json b/onlylegs/langs/gb.json
deleted file mode 100644
index d3f0895..0000000
--- a/onlylegs/langs/gb.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-    "IMAGES_UPLOADED": "%s images uploaded!",
-    "DONT USE THIS": "variable:format(data), jinja2 doesnt use the same method as Django does, odd"
-}
\ No newline at end of file
diff --git a/onlylegs/models.py b/onlylegs/models.py
index af3eb6f..6970a9b 100644
--- a/onlylegs/models.py
+++ b/onlylegs/models.py
@@ -6,18 +6,18 @@ from flask_login import UserMixin
 from onlylegs.extensions import db
 
 
-class GroupJunction(db.Model):  # pylint: disable=too-few-public-methods, C0103
+class AlbumJunction(db.Model):  # pylint: disable=too-few-public-methods, C0103
     """
-    Junction table for posts and groups
-    Joins with posts and groups
+    Junction table for picturess and albums
+    Joins with picturess and albums
     """
 
-    __tablename__ = "group_junction"
+    __tablename__ = "album_junction"
 
     id = db.Column(db.Integer, primary_key=True)
 
-    group_id = db.Column(db.Integer, db.ForeignKey("group.id"))
-    post_id = db.Column(db.Integer, db.ForeignKey("post.id"))
+    album_id = db.Column(db.Integer, db.ForeignKey("albums.id"))
+    picture_id = db.Column(db.Integer, db.ForeignKey("pictures.id"))
 
     date_added = db.Column(
         db.DateTime,
@@ -26,16 +26,15 @@ class GroupJunction(db.Model):  # pylint: disable=too-few-public-methods, C0103
     )
 
 
-class Post(db.Model):  # pylint: disable=too-few-public-methods, C0103
+class Pictures(db.Model):  # pylint: disable=too-few-public-methods, C0103
     """
-    Post table
+    Pictures table
     """
 
-    __tablename__ = "post"
+    __tablename__ = "pictures"
 
     id = db.Column(db.Integer, primary_key=True)
-
-    author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
+    author_id = db.Column(db.Integer, db.ForeignKey("users.id"))
 
     filename = db.Column(db.String, unique=True, nullable=False)
     mimetype = db.Column(db.String, nullable=False)
@@ -51,37 +50,37 @@ class Post(db.Model):  # pylint: disable=too-few-public-methods, C0103
         server_default=db.func.now(),  # pylint: disable=E1102
     )
 
-    junction = db.relationship("GroupJunction", backref="posts")
+    album_fk = db.relationship("AlbumJunction", backref="pictures")
 
 
-class Group(db.Model):  # pylint: disable=too-few-public-methods, C0103
+class Albums(db.Model):  # pylint: disable=too-few-public-methods, C0103
     """
-    Group table
+    albums table
     """
 
-    __tablename__ = "group"
+    __tablename__ = "albums"
 
     id = db.Column(db.Integer, primary_key=True)
 
     name = db.Column(db.String, nullable=False)
     description = db.Column(db.String, nullable=False)
 
-    author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
+    author_id = db.Column(db.Integer, db.ForeignKey("users.id"))
     created_at = db.Column(
         db.DateTime,
         nullable=False,
         server_default=db.func.now(),  # pylint: disable=E1102
     )
 
-    junction = db.relationship("GroupJunction", backref="groups")
+    album_fk = db.relationship("AlbumJunction", backref="albums")
 
 
-class User(db.Model, UserMixin):  # pylint: disable=too-few-public-methods, C0103
+class Users(db.Model, UserMixin):  # pylint: disable=too-few-public-methods, C0103
     """
-    User table
+    Users table
     """
 
-    __tablename__ = "user"
+    __tablename__ = "users"
 
     # Gallery used information
     id = db.Column(db.Integer, primary_key=True)
@@ -100,8 +99,8 @@ class User(db.Model, UserMixin):  # pylint: disable=too-few-public-methods, C010
         server_default=db.func.now(),  # pylint: disable=E1102
     )
 
-    posts = db.relationship("Post", backref="author")
-    groups = db.relationship("Group", backref="author")
+    pictures_fk = db.relationship("Pictures", backref="author")
+    albums_fk = db.relationship("Albums", backref="author")
 
     def get_id(self):
         return str(self.alt_id)
diff --git a/onlylegs/static/banner.png b/onlylegs/static/banner.png
index 179ed20..67aacfc 100644
Binary files a/onlylegs/static/banner.png and b/onlylegs/static/banner.png differ
diff --git a/onlylegs/static/fonts/Rubik.ttf b/onlylegs/static/fonts/Rubik.ttf
deleted file mode 100644
index 547f069..0000000
Binary files a/onlylegs/static/fonts/Rubik.ttf and /dev/null differ
diff --git a/onlylegs/static/fonts/font.css b/onlylegs/static/fonts/font.css
deleted file mode 100644
index bb8f61b..0000000
--- a/onlylegs/static/fonts/font.css
+++ /dev/null
@@ -1,7 +0,0 @@
-@font-face {
-    font-family: 'Rubik';
-    src: url('./Rubik.ttf') format('truetype');
-    font-style: normal;
-    font-display: block;
-    font-weight: 300 900;
-}
\ No newline at end of file
diff --git a/onlylegs/static/js/clipboard.js b/onlylegs/static/js/clipboard.js
new file mode 100644
index 0000000..0882f56
--- /dev/null
+++ b/onlylegs/static/js/clipboard.js
@@ -0,0 +1,8 @@
+function copyToClipboard(data) {
+    try {
+        navigator.clipboard.writeText(data)
+        addNotification("Copied to clipboard!", 4);
+    } catch (err) {
+        addNotification("Oh noes, something when wrong D:", 2);
+    }
+}
\ No newline at end of file
diff --git a/onlylegs/static/js/fade.js b/onlylegs/static/js/fade.js
new file mode 100644
index 0000000..56749ba
--- /dev/null
+++ b/onlylegs/static/js/fade.js
@@ -0,0 +1,11 @@
+// fade in images
+function imgFade(obj, time = 200) {
+    setTimeout(() => {
+        obj.style.animation = `imgFadeIn ${time}ms`;
+
+        setTimeout(() => {
+            obj.style.opacity = null;
+            obj.style.animation = null;
+        }, time);
+    }, 1);
+}
diff --git a/onlylegs/static/js/grid.js b/onlylegs/static/js/grid.js
new file mode 100644
index 0000000..54aae09
--- /dev/null
+++ b/onlylegs/static/js/grid.js
@@ -0,0 +1,88 @@
+function makeGrid() {
+    // Get the container and images
+    const container = document.querySelector('.gallery-grid');
+    const images = document.querySelectorAll('.gallery-item');
+
+    // Set the gap between images
+    const gap = 0.6 * 16;
+    const maxWidth = container.clientWidth - gap;
+    const maxHeight = 13 * 16;
+
+
+    if (window.innerWidth < 800) {
+        for (let i = 0; i < images.length; i++) {
+            images[i].style.height = images[i].offsetWidth + 'px';
+
+            images[i].style.width = null;
+            images[i].style.left = null;
+            images[i].style.top = null;
+        }
+        
+        container.style.height = null;
+        return;
+    }
+
+
+    // Calculate the width and height of each image
+    let calculated = {};
+    for (let i = 0; i < images.length; i++) {
+        const image = images[i].querySelector('img');
+        const width = image.naturalWidth;
+        const height = image.naturalHeight;
+
+        let ratio = width / height;
+        const newWidth = maxHeight * ratio;
+
+        if (newWidth > maxWidth) {
+            newWidth = maxWidth / 3 - gap; // 3 images per row max
+            ratio = newWidth / height;
+        }
+
+        calculated[i] = {"width": newWidth,
+                         "height": maxHeight,
+                         "ratio": ratio};
+    }
+
+    // Next images position
+    let nextTop = gap;
+    let nextLeft = gap;
+
+    for (let i = 0; i < images.length; i++) {
+        let currentRow = [];
+        let currectLength = 0;
+
+        // While the row is not full add images to it
+        while (currectLength < maxWidth && i !== images.length) {
+            currentRow.push(i);
+            currectLength += calculated[i]["width"];
+            i++;
+        }
+        
+        // Go back one image since the last one pushed the row over the limit
+        i--;
+
+        // Calculate the amount of space required to fill the row
+        const currentRowDiff = (currectLength - maxWidth);
+
+        // Cycle through the images in the row and adjust their width and left position
+        for (let j = 0; j < currentRow.length; j++) {
+            const image = images[currentRow[j]];
+            const data = calculated[currentRow[j]];
+            // Shrink compared to the % of the row it takes up
+            const shrink = currentRowDiff * (data["width"] / currectLength);
+
+            image.style.width = data["width"] - shrink - gap + 'px';
+            image.style.height = data["height"] + 'px';
+            image.style.left = nextLeft + 'px';
+            image.style.top = nextTop + 'px';
+
+            nextLeft += data["width"] - shrink;
+        }
+
+        // Reset for the next row
+        nextTop += maxHeight + gap;
+        nextLeft = gap;
+    }
+
+    container.style.height = nextTop + 'px';
+}
\ No newline at end of file
diff --git a/onlylegs/static/js/groupPage.js b/onlylegs/static/js/groupPage.js
new file mode 100644
index 0000000..193dccc
--- /dev/null
+++ b/onlylegs/static/js/groupPage.js
@@ -0,0 +1,155 @@
+function groupDeletePopup() {
+    let title = 'Yeet!';
+    let subtitle =
+        'Are you surrrre? This action is irreversible ' +
+        'and very final. This wont delete the images, ' +
+        'but it will remove them from this group.'
+    let body = null;
+
+    let deleteBtn = document.createElement('button');
+        deleteBtn.classList.add('btn-block');
+        deleteBtn.classList.add('critical');
+        deleteBtn.innerHTML = 'Dewww eeeet!';
+        deleteBtn.onclick = groupDeleteConfirm;
+
+    popupShow(title, subtitle, body, [popupCancelButton, deleteBtn]);
+}
+
+function groupDeleteConfirm(event) {
+    // AJAX takes control of subby form :3
+    event.preventDefault();
+
+    fetch('/group/' + group_data['id'], {
+        method: 'DELETE',
+    }).then(response => {
+        if (response.ok) {
+            window.location.href = '/group/';
+        } else {
+            addNotification('Server exploded, returned:' + response.status, 2);
+        }
+    }).catch(error => {
+        addNotification('Error yeeting group!' + error, 2);
+    });
+}
+
+function groupEditPopup() {
+    let title = 'Nothing stays the same';
+    let subtitle = 'Add, remove, or change, the power is in your hands...'
+
+    let formSubmitButton = document.createElement('button');
+        formSubmitButton.setAttribute('form', 'groupEditForm');
+        formSubmitButton.setAttribute('type', 'submit');
+        formSubmitButton.classList.add('btn-block');
+        formSubmitButton.classList.add('primary');
+        formSubmitButton.innerHTML = 'Saveeee';
+
+    // Create form
+    let body = document.createElement('form');
+        body.setAttribute('onsubmit', 'return groupEditConfirm(event);');
+        body.id = 'groupEditForm';
+
+    let formImageId = document.createElement('input');
+        formImageId.setAttribute('type', 'text');
+        formImageId.setAttribute('placeholder', 'Image ID');
+        formImageId.setAttribute('required', '');
+        formImageId.classList.add('input-block');
+        formImageId.id = 'groupFormImageId';
+
+    let formAction = document.createElement('input');
+        formAction.setAttribute('type', 'text');
+        formAction.setAttribute('value', 'add');
+        formAction.setAttribute('placeholder', '[add, remove]');
+        formAction.setAttribute('required', '');
+        formAction.classList.add('input-block');
+        formAction.id = 'groupFormAction';
+
+    body.appendChild(formImageId);
+    body.appendChild(formAction);
+
+    popupShow(title, subtitle, body, [popupCancelButton, formSubmitButton]);
+}
+
+function groupEditConfirm(event) {
+    // AJAX takes control of subby form :3
+    event.preventDefault();
+
+    let imageId = document.querySelector("#groupFormImageId").value;
+    let action = document.querySelector("#groupFormAction").value;
+    let formData = new FormData();
+        formData.append("imageId", imageId);
+        formData.append("action", action);
+
+    fetch('/group/' + group_data['id'], {
+        method: 'PUT',
+        body: formData
+    }).then(response => {
+       if (response.ok) {
+            window.location.reload();
+        } else {
+            addNotification('Server exploded, returned:' + response.status, 2);
+        }
+    }).catch(error => {
+        addNotification('Error!!!!! Panic!!!!' + error, 2);
+    });
+}
+
+function groupCreatePopup() {
+    let title = 'New stuff!';
+    let subtitle =
+        'Image groups are a simple way to ' +
+        '"group" images together, are you ready?'
+
+    let formSubmitButton = document.createElement('button');
+        formSubmitButton.setAttribute('form', 'groupCreateForm');
+        formSubmitButton.setAttribute('type', 'submit');
+        formSubmitButton.classList.add('btn-block');
+        formSubmitButton.classList.add('primary');
+        formSubmitButton.innerHTML = 'Huzzah!';
+
+    // Create form
+    let body = document.createElement('form');
+        body.setAttribute('onsubmit', 'return groupCreateConfirm(event);');
+        body.id = 'groupCreateForm';
+
+    let formName = document.createElement('input');
+        formName.setAttribute('type', 'text');
+        formName.setAttribute('placeholder', 'Group namey');
+        formName.setAttribute('required', '');
+        formName.classList.add('input-block');
+        formName.id = 'groupFormName';
+
+    let formDescription = document.createElement('input');
+        formDescription.setAttribute('type', 'text');
+        formDescription.setAttribute('placeholder', 'What it about????');
+        formDescription.classList.add('input-block');
+        formDescription.id = 'groupFormDescription';
+
+    body.appendChild(formName);
+    body.appendChild(formDescription);
+
+    popupShow(title, subtitle, body, [popupCancelButton, formSubmitButton]);
+}
+
+function groupCreateConfirm(event) {
+    // AJAX takes control of subby form :3
+    event.preventDefault();
+
+    let name = document.querySelector("#groupFormName").value;
+    let description = document.querySelector("#groupFormDescription").value;
+    let formData = new FormData();
+        formData.append("name", name);
+        formData.append("description", description);
+
+    fetch('/group/', {
+        method: 'POST',
+        body: formData
+    }).then(response => {
+        if (response.ok) {
+            window.location.reload();
+        } else {
+            addNotification('Server exploded, returned:' + response.status, 2);
+        }
+    }).catch(error => {
+        addNotification('Error summoning group!' + error, 2);
+    });
+}
\ No newline at end of file
diff --git a/onlylegs/static/js/imagePage.js b/onlylegs/static/js/imagePage.js
new file mode 100644
index 0000000..b98d46d
--- /dev/null
+++ b/onlylegs/static/js/imagePage.js
@@ -0,0 +1,101 @@
+function imageFullscreen() {
+    let info = document.querySelector('.info-container');
+    let image = document.querySelector('.image-container');
+
+    if (info.classList.contains('collapsed')) {
+        info.classList.remove('collapsed');
+        image.classList.remove('collapsed');
+        document.cookie = "image-info=0"
+    } else {
+        info.classList.add('collapsed');
+        image.classList.add('collapsed');
+        document.cookie = "image-info=1"
+    }
+}
+function imageDeletePopup() {
+    let title = 'DESTRUCTION!!!!!!';
+    let subtitle =
+        'Do you want to delete this image along with ' +
+        'all of its data??? This action is irreversible!';
+    let body = null;
+
+    let deleteBtn = document.createElement('button');
+        deleteBtn.classList.add('btn-block');
+        deleteBtn.classList.add('critical');
+        deleteBtn.innerHTML = 'Dewww eeeet!';
+        deleteBtn.onclick = imageDeleteConfirm;
+
+    popupShow(title, subtitle, body, [popupCancelButton, deleteBtn]);
+}
+function imageDeleteConfirm() {
+    popupDismiss();
+
+    fetch('/image/' + image_data["id"], {
+        method: 'DELETE',
+    }).then(response => {
+        if (response.ok) {
+            window.location.href = '/';
+        } else {
+            addNotification('Image *clings*', 2);
+        }
+    });
+}
+
+function imageEditPopup() {
+    let title = 'Edit image!';
+    let subtitle = 'Enter funny stuff here!';
+
+    let formSubmitButton = document.createElement('button');
+        formSubmitButton.setAttribute('form', 'imageEditForm');
+        formSubmitButton.setAttribute('type', 'submit');
+        formSubmitButton.classList.add('btn-block');
+        formSubmitButton.classList.add('primary');
+        formSubmitButton.innerHTML = 'Saveeee';
+
+    // Create form
+    let body = document.createElement('form');
+        body.setAttribute('onsubmit', 'return imageEditConfirm(event);');
+        body.id = 'imageEditForm';
+
+    let formAlt = document.createElement('input');
+        formAlt.setAttribute('type', 'text');
+        formAlt.setAttribute('value', image_data["alt"]);
+        formAlt.setAttribute('placeholder', 'Image Alt');
+        formAlt.classList.add('input-block');
+        formAlt.id = 'imageFormAlt';
+
+    let formDescription = document.createElement('input');
+        formDescription.setAttribute('type', 'text');
+        formDescription.setAttribute('value', image_data["description"]);
+        formDescription.setAttribute('placeholder', 'Image Description');
+        formDescription.classList.add('input-block');
+        formDescription.id = 'imageFormDescription';
+
+    body.appendChild(formAlt);
+    body.appendChild(formDescription);
+
+    popupShow(title, subtitle, body, [popupCancelButton, formSubmitButton]);
+}
+
+function imageEditConfirm(event) {
+    // Yoink subby form
+    event.preventDefault();
+
+    let alt = document.querySelector('#imageFormAlt').value;
+    let description = document.querySelector('#imageFormDescription').value;
+    let form = new FormData();
+        form.append('alt', alt);
+        form.append('description', description);
+
+    fetch('/image/' + image_data["id"], {
+        method: 'PUT',
+        body: form,
+    }).then(response => {
+        if (response.ok) {
+            popupDismiss();
+            window.location.reload();
+        } else {
+            addNotification('Image *clings*', 2);
+        }
+    });
+}
diff --git a/onlylegs/static/js/index.js b/onlylegs/static/js/index.js
deleted file mode 100644
index d9d0aa5..0000000
--- a/onlylegs/static/js/index.js
+++ /dev/null
@@ -1,93 +0,0 @@
-// fade in images
-function imgFade(obj, time = 250) {
-    obj.style.transition = `opacity ${time}ms`;
-    obj.style.opacity = 1;
-}
-// Lazy load images when they are in view
-function loadOnView() {
-    const lazyLoad = document.querySelectorAll('#lazy-load');
-    const webpSupport = checkWebpSupport();
-
-    for (let i = 0; i < lazyLoad.length; i++) {
-        const image = lazyLoad[i];
-        if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) {
-            if (!image.src && webpSupport) {
-                image.src = `${image.getAttribute('data-src')}&e=webp`;
-            } else if (!image.src) {
-                image.src = image.getAttribute('data-src');
-            }
-        }
-    }
-}
-
-window.onload = function () {
-    loadOnView();
-
-    const times = document.querySelectorAll('.time');
-    for (let i = 0; i < times.length; i++) {
-        // Remove milliseconds
-        const raw = times[i].innerHTML.split('.')[0];
-
-        // Parse YYYY-MM-DD HH:MM:SS to Date object
-        const time = raw.split(' ')[1];
-        const date = raw.split(' ')[0].split('-');
-
-        // Format to YYYY/MM/DD HH:MM:SS and convert to UTC Date object
-        const dateTime = new Date(`${date[0]}/${date[1]}/${date[2]} ${time} UTC`);
-
-        // Convert to local time
-        times[i].innerHTML = `${dateTime.toLocaleDateString()} ${dateTime.toLocaleTimeString()}`;
-    }
-
-    // Top Of Page button
-    const topOfPage = document.querySelector('.top-of-page');
-    if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
-        topOfPage.classList.add('show');
-    } else {
-        topOfPage.classList.remove('show');
-    }
-    topOfPage.onclick = function () {
-        document.body.scrollTop = 0;
-        document.documentElement.scrollTop = 0;
-    }
-
-    // Info button
-    const infoButton = document.querySelector('.info-button');
-    if (infoButton) {
-        if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
-            infoButton.classList.remove('show');
-        } else {
-            infoButton.classList.add('show');
-        }
-        infoButton.onclick = function () {
-            popUpShow('OnlyLegs',
-                      '<a href="https://github.com/Fluffy-Bean/onlylegs">v0.1.2</a> ' +
-                      'using <a href="https://phosphoricons.com/">Phosphoricons</a> and Flask.' +
-                      '<br>Made by Fluffy and others with ❤️');
-        }
-    }
-};
-window.onscroll = function () {
-    loadOnView();
-
-    // Top Of Page button
-    const topOfPage = document.querySelector('.top-of-page');
-    if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
-        topOfPage.classList.add('show');
-    } else {
-        topOfPage.classList.remove('show');
-    }
-
-    // Info button
-    const infoButton = document.querySelector('.info-button');
-    if (infoButton) {
-        if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
-            infoButton.classList.remove('show');
-        } else {
-            infoButton.classList.add('show');
-        }
-    }
-};
-window.onresize = function () {
-    loadOnView();
-};
diff --git a/onlylegs/static/js/login.js b/onlylegs/static/js/login.js
index 1e2287a..ed95512 100644
--- a/onlylegs/static/js/login.js
+++ b/onlylegs/static/js/login.js
@@ -5,7 +5,7 @@ function showLogin() {
     cancelBtn.classList.add('btn-block');
     cancelBtn.classList.add('transparent');
     cancelBtn.innerHTML = 'nuuuuuuuu';
-    cancelBtn.onclick = popupDissmiss;
+    cancelBtn.onclick = popupDismiss;
 
     loginBtn = document.createElement('button');
     loginBtn.classList.add('btn-block');
@@ -50,7 +50,7 @@ function showLogin() {
     loginForm.appendChild(passwordInput);
     loginForm.appendChild(rememberMeSpan);
 
-    popUpShow(
+    popupShow(
         'Login!',
         'Need an account? <span class="link" onclick="showRegister()">Register!</span>',
         loginForm,
@@ -103,7 +103,7 @@ function showRegister() {
     cancelBtn.classList.add('btn-block');
     cancelBtn.classList.add('transparent');
     cancelBtn.innerHTML = 'nuuuuuuuu';
-    cancelBtn.onclick = popupDissmiss;
+    cancelBtn.onclick = popupDismiss;
 
     registerBtn = document.createElement('button');
     registerBtn.classList.add('btn-block');
@@ -146,7 +146,7 @@ function showRegister() {
     registerForm.appendChild(passwordInput);
     registerForm.appendChild(passwordInputRepeat);
     
-    popUpShow(
+    popupShow(
         'Who are you?',
         'Already have an account? <span class="link" onclick="showLogin()">Login!</span>',
         registerForm,
diff --git a/onlylegs/static/js/popup.js b/onlylegs/static/js/popup.js
index b0b19ac..0ef8cc4 100644
--- a/onlylegs/static/js/popup.js
+++ b/onlylegs/static/js/popup.js
@@ -1,4 +1,4 @@
-function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null) {
+function popupShow(titleText, subtitleText, bodyContent=null, userActions=null) {
     // Get popup elements
     const popupSelector = document.querySelector('.pop-up');
     const headerSelector = document.querySelector('.pop-up-header');
@@ -9,38 +9,47 @@ function popUpShow(titleText, subtitleText, bodyContent=null, userActions=null)
     actionsSelector.innerHTML = '';
 
     // Set popup header and subtitle
-    const titleElement = document.createElement('h2');
-    titleElement.innerHTML = titleText;
-    headerSelector.appendChild(titleElement);
+    let titleElement = document.createElement('h2');
+        titleElement.innerHTML = titleText;
+        headerSelector.appendChild(titleElement);
 
-    const subtitleElement = document.createElement('p');
-    subtitleElement.innerHTML = subtitleText;
-    headerSelector.appendChild(subtitleElement);
+    let subtitleElement = document.createElement('p');
+        subtitleElement.innerHTML = subtitleText;
+        headerSelector.appendChild(subtitleElement);
 
-    if (bodyContent) {
-        headerSelector.appendChild(bodyContent);
-    }
+    if (bodyContent) { headerSelector.appendChild(bodyContent) }
 
     // Set buttons that will be displayed
     if (userActions) {
-        // for each user action, add the element
-        for (let i = 0; i < userActions.length; i++) {
-            actionsSelector.appendChild(userActions[i]);
-        }
+        userActions.forEach((action) => {
+            actionsSelector.appendChild(action);
+        });
     } else {
-        actionsSelector.innerHTML = '<button class="btn-block transparent" onclick="popupDissmiss()">Close</button>';
+        let closeButton = document.createElement('button');
+            closeButton.classList.add('btn-block');
+            closeButton.classList.add('transparent');
+            closeButton.innerHTML = 'Yeet!';
+            closeButton.onclick = popupDismiss;
+        actionsSelector.appendChild(closeButton);
     }
 
     // Stop scrolling and show popup
     document.querySelector("html").style.overflow = "hidden";
     popupSelector.style.display = 'block';
-    setTimeout(() => { popupSelector.classList.add('active') }, 5);  // 2ms delay to allow for css transition >:C
+
+    // 5ms delay to allow for css transition >:C
+    setTimeout(() => { popupSelector.classList.add('active') }, 5);
 }
 
-function popupDissmiss() {
+function popupDismiss() {
     const popupSelector = document.querySelector('.pop-up');
-
     document.querySelector("html").style.overflow = "auto";
     popupSelector.classList.remove('active');
     setTimeout(() => { popupSelector.style.display = 'none'; }, 200);
 }
+
+const popupCancelButton = document.createElement('button');
+      popupCancelButton.classList.add('btn-block');
+      popupCancelButton.classList.add('transparent');
+      popupCancelButton.innerHTML = 'nuuuuuuuu';
+      popupCancelButton.onclick = popupDismiss;
diff --git a/onlylegs/static/js/square.js b/onlylegs/static/js/square.js
new file mode 100644
index 0000000..ff9bfb4
--- /dev/null
+++ b/onlylegs/static/js/square.js
@@ -0,0 +1,6 @@
+function keepSquare() {
+    let square = document.getElementsByClassName('square')
+    for (let i = 0; i < square.length; i++) {
+        square[i].style.height = square[i].offsetWidth + 'px';
+    }
+}
\ No newline at end of file
diff --git a/onlylegs/static/js/webp.js b/onlylegs/static/js/webp.js
deleted file mode 100644
index 93a4ade..0000000
--- a/onlylegs/static/js/webp.js
+++ /dev/null
@@ -1,10 +0,0 @@
-function checkWebpSupport() {
-    let webpSupport = false;
-    try {
-        webpSupport = document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') === 0;
-    } catch (e) {
-        webpSupport = false;
-    }
-
-    return webpSupport;
-}
diff --git a/onlylegs/static/logo-black.svg b/onlylegs/static/logo-black.svg
deleted file mode 100644
index 559ad4d..0000000
--- a/onlylegs/static/logo-black.svg
+++ /dev/null
@@ -1,60 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   version="1.0"
-   width="621.000000pt"
-   height="621.000000pt"
-   viewBox="0 0 621.000000 621.000000"
-   preserveAspectRatio="xMidYMid meet"
-   id="svg12"
-   sodipodi:docname="OnlyLegs.svg"
-   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:svg="http://www.w3.org/2000/svg">
-  <defs
-     id="defs16" />
-  <sodipodi:namedview
-     id="namedview14"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:showpageshadow="2"
-     inkscape:pageopacity="0.0"
-     inkscape:pagecheckerboard="0"
-     inkscape:deskcolor="#d1d1d1"
-     inkscape:document-units="pt"
-     showgrid="false"
-     inkscape:zoom="0.75493043"
-     inkscape:cx="207.30387"
-     inkscape:cy="449.048"
-     inkscape:window-width="1856"
-     inkscape:window-height="1064"
-     inkscape:window-x="56"
-     inkscape:window-y="8"
-     inkscape:window-maximized="1"
-     inkscape:current-layer="g10" />
-  <g
-     transform="translate(0.000000,621.000000) scale(0.100000,-0.100000)"
-     fill="#000000"
-     stroke="none"
-     id="g10">
-    <path
-       d="m 1650,6136 c -28,-16 -61,-77 -113,-206 -107,-264 -114,-362 -48,-634 23,-93 46,-204 52,-245 6,-42 17,-104 25,-137 7,-34 19,-97 24,-140 6,-44 20,-124 30,-179 11,-55 27,-138 35,-185 8,-47 24,-125 35,-175 12,-49 29,-132 39,-183 18,-91 25.8809,-120.7949 21.7873,-134.9298 -10.1829,-12.5128 -43.6012,-16.8559 -57.7873,-18.0702 -299,-22 -419,-42 -480,-80 -57,-36 -69,-86 -32,-133 24,-30 137,-88 215,-111 27,-7 74,-22 104,-33 63,-23 67,-21 -90,-47 -107,-18 -264,-59 -327,-86 -71,-30 -97,-116 -49,-164 36,-36 84,-48 364,-91 29,-4 32,-8 32,-39 0,-55 20,-85 74,-112 141.9762,-39.3457 220.3199,-36.183 366,-42 -52.2181,-55.8508 -428.3582,-414.6946 -490.4542,-467.477 -65.1534,47.6205 -260.6727,12.2815 -311.2588,-90.0814 -50.3326,-175.9923 22.0801,-322.4949 253.1708,-312.892 82.63,4.7723 122.7958,22.9511 163.5422,72.4504 110.0316,28.4289 1032.7396,235.9943 1114,257 14,0 79,-71 100,-109 148.2757,-235.4969 289.4861,-410.3884 407,-631 31,-68 62,-134 70,-145 7,-11 32,-74 54,-140 45,-128 68,-283 69,-451 1,-104 16,-126 136,-211 144,-101 193,-121 222,-92 15,15 17,0 55,-493 L 3724,0 h 79 80 l -7,123 c -4,67 -11,151 -16,187 -5,36 -14,137 -20,225 -6,88 -15,176 -21,196 -5,20 -20,112 -33,203 -26,188 -35,206 -97,206 -29,0 -42,-6 -58,-26 -25,-32 -26,-50 -6,-180 23,-156 25,-152 -44,-102 -33,24 -73,51 -90,60 -30,16 -31,20 -32,85 -1,166 -25,313 -73,458 -27,77 -54,150 -62,162 -8,13 -98.9126,185.5819 -130.9126,255.5819 C 3078.1155,2027.0841 3018.0028,2099.9913 2958,2188 c -60.9556,77.2514 -67.3555,103.6675 -109.5679,174.1092 L 2791,2467 l 87,26 c 92,28 219,53 342,69 82,10 115,26 132,64 14,30 0,66 -37,90 -23,15 -37,16 -108,7 -112,-14 -286,-50 -382,-78 -44,-13 -96,-26 -115,-30 -19,-4 -114,-28 -210,-54 -66.7812,-17.1766 -734.4826,-161.2693 -877.9113,-194.6921 -61.4817,-10.1244 -101.9911,25.0397 -69.4824,79.572 30.1454,40.2606 436.3495,409.3445 464.3937,438.1201 18,31 16,60 -8,90 -17.2726,26.1357 -4.8315,31.9291 30,50 53.754,58.4845 37.1768,154.6092 -116,123 -43,-9 -112,-16 -153,-16 -64,1 -69,2 -36,9 77,15 100,87 47,141 -44.2079,53.7356 -129.2499,29.5932 -198,30 -221,1 -246,9 -107,33 231,40 352,84 379,136 45,87 -15,141 -205,185 -41,10 -93,25 -115,34 l -40,16 50,8 c 28,4 122,12 210,18 187,12 208,21 213,86 4,54 -30,83 -97,83 -43,0 -45,1 -21,13 13,7 31,24 40,39 13,24 13,37 -2,120 -9,51 -26,131 -38,178 -21,84 -53,254 -85,445 -9,55 -27,156 -40,225 -13,69 -26,148 -30,175 -7,49 -42,209 -70,315 -19,74 -26,158 -14,165 5,4 22,-7 38,-24 37,-39 81,-42 116,-6 14,13 25,32 25,41 0,12 5,14 18,9 47,-21 71,-17 102,13 21,21 30,40 30,62 0,30 6,24 88,-79 89,-114 175,-258 286,-486 71,-143 131,-332 170,-531 27,-141 35,-158 76,-173 33,-11 55,-7 81,16 15,12 20,10 47,-17 36,-35 82,-39 172,-15 60,16 283,20 405,6 129,-14 389,-90 542,-157 36,-16 74,-10 97,14 8,10 41,69 74,132 33,63 76,141 95,174 20,32 42,74 48,93 11,33 103,189 175,298 95,143 226,309 330,417 l 83,88 26,-26 c 20,-20 34,-25 60,-22 29,3 35,0 40,-19 8,-32 49,-56 86,-51 27,4 34,0 51,-29 22,-37 46,-53 77,-53 12,0 21,-3 21,-7 0,-5 -18,-69 -40,-143 -29,-95 -71,-197 -142,-345 -55,-115 -115,-246 -133,-290 -40,-101 -44,-108 -125,-235 -57.6611,-76.9253 -108.8483,-155.9466 -158,-245 -41.8198,-72.4341 -151.3026,-225.4907 -108,-300 65.0126,-70.4405 142.2339,-55.61 217,-79 24,-8 70,-20 103,-26 32,-7 54,-16 50,-20 -5,-5 -54.4462,-13.6036 -109,-25 -55.5759,-11.6099 -107.6919,-23.3581 -190,-41 -86.0096,-13.9667 -223.7414,-29.6671 -275,-106 -59,-97 -1,-143 260,-211 l 55,-14 -55,-13 c -165,-38 -232,-110 -185,-199 14,-28 13,-29 -13,-39 -15,-6 -49,-15 -77,-22 -65,-14 -153,-72 -161,-105 -13,-51 31,-105 84,-105 10,0 34,11 54,25 20,14 64,31 96,39 l 59,14 17,-83 c 9,-46 16,-107 16,-135 0,-44 4,-56 29,-81 37,-37 65,-37 102,0 27,27 29,35 29,105 0,41 -4,85 -9,98 -4,13 -11,47 -15,76 l -7,52 c 180.2886,6.4837 207.2871,-4.1989 355.8174,23.0309 66,12 305.1826,43.9691 335.1826,52.9691 85,26 170,34 170,16 0,-8 15,-29 34,-48 59,-59 104,-45 271,80 117,89 129,91 178,35 21,-24 59,-74 83,-111 30,-44 62,-77 97,-100 48,-31 58,-33 149,-36 l 98,-4 v 85 85 l -66,-6 c -82,-6 -102,4 -150,77 -160,243 -270,269 -476,113 l -66,-50 -13,29 c -12,31 -54,59 -86,59 -10,0 -34,-8 -53,-18 -19,-11 -62,-22 -95,-25 -33,-3 -80,-13 -104,-22 -25,-8 -97,-24 -160,-36 -150.6695,-31.5585 -331.7946,-51.6255 -476.3702,-37.9738 -66,9 53.3702,64.9738 155.3702,82.9738 221,38 324,64 342,86 31,38 17,107 -26,129 -33,17 -144,40 -226,46 -41,4 -95,10 -120,15 -25,5 -65,12 -90,16 -39,6 -42,8 -22,15 12,5 53,13 90,19 138,23 319,62 386,83 77,25 136,78 136,123 0,35 -47,80 -101,94 -24,7 -80,23 -124,36 -44,13 -136,38 -205,55 -69,17 -128,33 -133,36 -4,2 18,32 49,66 30,34 78,103 106,152 28,50 67,115 88,145 45,68 103,173 120,220 7,19 66,148 131,285 216,458 279,758 208,1000 -12,39 -27,81 -34,95 -7,14 -21,48 -30,75 -26,78 -35,93 -68,106 -58,24 -139,-13 -239,-108 -311,-298 -478,-484 -659,-735 -65,-90 -214,-337 -228,-377 -6,-19 -35,-75 -64,-125 -30,-50 -62,-110 -72,-134 -10,-24 -23,-42 -29,-40 -6,2 -69,22 -141,45 -163,52 -290,84 -380,94 -124,14 -394,10 -455,-6 -30,-8 -67,-15 -81,-15 -15,0 -38,-9 -52,-20 -14,-11 -28,-17 -31,-12 -2,4 -14,55 -25,112 -54,270 -139,504 -262,723 -28,51 -71,127 -95,169 -54,95 -275,380 -364,468 -36,36 -93,89 -128,118 -67,55 -96,63 -142,38 z"
-       id="path2"
-       sodipodi:nodetypes="cccccccsccccccccccscscccccccccccccscccccssccscccccccccccccccccscccccccccccccccccccscccccscccccsccsccccccscccscccccsccccssccccccccsscccccccccsscccsssscccccsscscccccccccccscscccccccsccccscsscccccccsscccccsscccssccccccc" />
-    <path
-       d="m 2500.8313,4134.78 c -87.3956,-37.9843 -97.6852,-154.4873 -98,-194.7871 0,-44.3118 -4,-131.089 -9,-192.0177 -5,-60.9286 -5,-159.707 -2,-219.7125 6,-105.2404 7,-110.7794 34,-132.0121 79,-62.775 164,3.6926 217,168.9386 55,175.4007 49,403.4217 -15,519.7401 -35,64.6213 -71,78.4687 -127,49.8507 z"
-       id="path4"
-       sodipodi:nodetypes="ccsccccc"
-       style="stroke-width:1" />
-    <path
-       d="m 3344.4273,3996.6247 c -48.5583,-50.0953 -37.4868,-100.9153 -40.8178,-215.7915 -2.7697,-95.5212 29.3536,-310.9299 34.084,-408.9796 38.3413,-101.4275 148.4445,-115.5653 198.0212,-37.0586 31.0725,92.512 58.8307,202.0641 62.5807,328.6811 2.6164,88.3394 -12.9222,191.6407 -19.8758,275.1699 -22.7001,117.379 -144.8343,172.9775 -233.9923,57.9787 z"
-       id="path6"
-       sodipodi:nodetypes="csccscc"
-       style="stroke-width:1" />
-    <path
-       d="M4633 1040 c-60 -25 -78 -94 -158 -605 -20 -126 -45 -273 -56 -327 -10 -53 -19 -99 -19 -102 0 -3 41 -6 91 -6 85 0 90 1 84 19 -3 11 3 66 14 123 10 57 24 142 31 190 l12 87 46 -5 c111 -12 107 -10 141 -74 37 -68 81 -231 81 -296 l0 -44 85 0 85 0 -6 42 c-10 75 -44 237 -60 280 -8 22 -12 43 -9 46 2 2 46 -1 97 -7 181 -22 397 -31 756 -31 l362 0 0 80 0 80 -362 0 c-388 0 -565 9 -828 41 -183 21 -351 46 -357 52 -10 11 46 278 73 342 16 39 11 76 -14 98 -23 21 -62 28 -89 17z"
-       id="path8" />
-  </g>
-</svg>
diff --git a/onlylegs/static/logo-white.svg b/onlylegs/static/logo-white.svg
deleted file mode 100644
index a50b3f3..0000000
--- a/onlylegs/static/logo-white.svg
+++ /dev/null
@@ -1,62 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   version="1.0"
-   width="621.000000pt"
-   height="621.000000pt"
-   viewBox="0 0 621.000000 621.000000"
-   preserveAspectRatio="xMidYMid meet"
-   id="svg12"
-   sodipodi:docname="OnlyLegs-white.svg"
-   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:svg="http://www.w3.org/2000/svg">
-  <defs
-     id="defs16" />
-  <sodipodi:namedview
-     id="namedview14"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:showpageshadow="2"
-     inkscape:pageopacity="0.0"
-     inkscape:pagecheckerboard="0"
-     inkscape:deskcolor="#d1d1d1"
-     inkscape:document-units="pt"
-     showgrid="false"
-     inkscape:zoom="0.75493043"
-     inkscape:cx="207.30387"
-     inkscape:cy="449.048"
-     inkscape:window-width="1856"
-     inkscape:window-height="1064"
-     inkscape:window-x="56"
-     inkscape:window-y="8"
-     inkscape:window-maximized="1"
-     inkscape:current-layer="g10" />
-  <g
-     transform="translate(0.000000,621.000000) scale(0.100000,-0.100000)"
-     fill="#000000"
-     stroke="none"
-     id="g10">
-    <path
-       d="m 1650,6136 c -28,-16 -61,-77 -113,-206 -107,-264 -114,-362 -48,-634 23,-93 46,-204 52,-245 6,-42 17,-104 25,-137 7,-34 19,-97 24,-140 6,-44 20,-124 30,-179 11,-55 27,-138 35,-185 8,-47 24,-125 35,-175 12,-49 29,-132 39,-183 18,-91 25.8809,-120.7949 21.7873,-134.9298 -10.1829,-12.5128 -43.6012,-16.8559 -57.7873,-18.0702 -299,-22 -419,-42 -480,-80 -57,-36 -69,-86 -32,-133 24,-30 137,-88 215,-111 27,-7 74,-22 104,-33 63,-23 67,-21 -90,-47 -107,-18 -264,-59 -327,-86 -71,-30 -97,-116 -49,-164 36,-36 84,-48 364,-91 29,-4 32,-8 32,-39 0,-55 20,-85 74,-112 141.9762,-39.3457 220.3199,-36.183 366,-42 -52.2181,-55.8508 -428.3582,-414.6946 -490.4542,-467.477 -65.1534,47.6205 -260.6727,12.2815 -311.2588,-90.0814 -50.3326,-175.9923 22.0801,-322.4949 253.1708,-312.892 82.63,4.7723 122.7958,22.9511 163.5422,72.4504 110.0316,28.4289 1032.7396,235.9943 1114,257 14,0 79,-71 100,-109 148.2757,-235.4969 289.4861,-410.3884 407,-631 31,-68 62,-134 70,-145 7,-11 32,-74 54,-140 45,-128 68,-283 69,-451 1,-104 16,-126 136,-211 144,-101 193,-121 222,-92 15,15 17,0 55,-493 L 3724,0 h 79 80 l -7,123 c -4,67 -11,151 -16,187 -5,36 -14,137 -20,225 -6,88 -15,176 -21,196 -5,20 -20,112 -33,203 -26,188 -35,206 -97,206 -29,0 -42,-6 -58,-26 -25,-32 -26,-50 -6,-180 23,-156 25,-152 -44,-102 -33,24 -73,51 -90,60 -30,16 -31,20 -32,85 -1,166 -25,313 -73,458 -27,77 -54,150 -62,162 -8,13 -98.9126,185.5819 -130.9126,255.5819 C 3078.1155,2027.0841 3018.0028,2099.9913 2958,2188 c -60.9556,77.2514 -67.3555,103.6675 -109.5679,174.1092 L 2791,2467 l 87,26 c 92,28 219,53 342,69 82,10 115,26 132,64 14,30 0,66 -37,90 -23,15 -37,16 -108,7 -112,-14 -286,-50 -382,-78 -44,-13 -96,-26 -115,-30 -19,-4 -114,-28 -210,-54 -66.7812,-17.1766 -734.4826,-161.2693 -877.9113,-194.6921 -61.4817,-10.1244 -101.9911,25.0397 -69.4824,79.572 30.1454,40.2606 436.3495,409.3445 464.3937,438.1201 18,31 16,60 -8,90 -17.2726,26.1357 -4.8315,31.9291 30,50 53.754,58.4845 37.1768,154.6092 -116,123 -43,-9 -112,-16 -153,-16 -64,1 -69,2 -36,9 77,15 100,87 47,141 -44.2079,53.7356 -129.2499,29.5932 -198,30 -221,1 -246,9 -107,33 231,40 352,84 379,136 45,87 -15,141 -205,185 -41,10 -93,25 -115,34 l -40,16 50,8 c 28,4 122,12 210,18 187,12 208,21 213,86 4,54 -30,83 -97,83 -43,0 -45,1 -21,13 13,7 31,24 40,39 13,24 13,37 -2,120 -9,51 -26,131 -38,178 -21,84 -53,254 -85,445 -9,55 -27,156 -40,225 -13,69 -26,148 -30,175 -7,49 -42,209 -70,315 -19,74 -26,158 -14,165 5,4 22,-7 38,-24 37,-39 81,-42 116,-6 14,13 25,32 25,41 0,12 5,14 18,9 47,-21 71,-17 102,13 21,21 30,40 30,62 0,30 6,24 88,-79 89,-114 175,-258 286,-486 71,-143 131,-332 170,-531 27,-141 35,-158 76,-173 33,-11 55,-7 81,16 15,12 20,10 47,-17 36,-35 82,-39 172,-15 60,16 283,20 405,6 129,-14 389,-90 542,-157 36,-16 74,-10 97,14 8,10 41,69 74,132 33,63 76,141 95,174 20,32 42,74 48,93 11,33 103,189 175,298 95,143 226,309 330,417 l 83,88 26,-26 c 20,-20 34,-25 60,-22 29,3 35,0 40,-19 8,-32 49,-56 86,-51 27,4 34,0 51,-29 22,-37 46,-53 77,-53 12,0 21,-3 21,-7 0,-5 -18,-69 -40,-143 -29,-95 -71,-197 -142,-345 -55,-115 -115,-246 -133,-290 -40,-101 -44,-108 -125,-235 -57.6611,-76.9253 -108.8483,-155.9466 -158,-245 -41.8198,-72.4341 -151.3026,-225.4907 -108,-300 65.0126,-70.4405 142.2339,-55.61 217,-79 24,-8 70,-20 103,-26 32,-7 54,-16 50,-20 -5,-5 -54.4462,-13.6036 -109,-25 -55.5759,-11.6099 -107.6919,-23.3581 -190,-41 -86.0096,-13.9667 -223.7414,-29.6671 -275,-106 -59,-97 -1,-143 260,-211 l 55,-14 -55,-13 c -165,-38 -232,-110 -185,-199 14,-28 13,-29 -13,-39 -15,-6 -49,-15 -77,-22 -65,-14 -153,-72 -161,-105 -13,-51 31,-105 84,-105 10,0 34,11 54,25 20,14 64,31 96,39 l 59,14 17,-83 c 9,-46 16,-107 16,-135 0,-44 4,-56 29,-81 37,-37 65,-37 102,0 27,27 29,35 29,105 0,41 -4,85 -9,98 -4,13 -11,47 -15,76 l -7,52 c 180.2886,6.4837 207.2871,-4.1989 355.8174,23.0309 66,12 305.1826,43.9691 335.1826,52.9691 85,26 170,34 170,16 0,-8 15,-29 34,-48 59,-59 104,-45 271,80 117,89 129,91 178,35 21,-24 59,-74 83,-111 30,-44 62,-77 97,-100 48,-31 58,-33 149,-36 l 98,-4 v 85 85 l -66,-6 c -82,-6 -102,4 -150,77 -160,243 -270,269 -476,113 l -66,-50 -13,29 c -12,31 -54,59 -86,59 -10,0 -34,-8 -53,-18 -19,-11 -62,-22 -95,-25 -33,-3 -80,-13 -104,-22 -25,-8 -97,-24 -160,-36 -150.6695,-31.5585 -331.7946,-51.6255 -476.3702,-37.9738 -66,9 53.3702,64.9738 155.3702,82.9738 221,38 324,64 342,86 31,38 17,107 -26,129 -33,17 -144,40 -226,46 -41,4 -95,10 -120,15 -25,5 -65,12 -90,16 -39,6 -42,8 -22,15 12,5 53,13 90,19 138,23 319,62 386,83 77,25 136,78 136,123 0,35 -47,80 -101,94 -24,7 -80,23 -124,36 -44,13 -136,38 -205,55 -69,17 -128,33 -133,36 -4,2 18,32 49,66 30,34 78,103 106,152 28,50 67,115 88,145 45,68 103,173 120,220 7,19 66,148 131,285 216,458 279,758 208,1000 -12,39 -27,81 -34,95 -7,14 -21,48 -30,75 -26,78 -35,93 -68,106 -58,24 -139,-13 -239,-108 -311,-298 -478,-484 -659,-735 -65,-90 -214,-337 -228,-377 -6,-19 -35,-75 -64,-125 -30,-50 -62,-110 -72,-134 -10,-24 -23,-42 -29,-40 -6,2 -69,22 -141,45 -163,52 -290,84 -380,94 -124,14 -394,10 -455,-6 -30,-8 -67,-15 -81,-15 -15,0 -38,-9 -52,-20 -14,-11 -28,-17 -31,-12 -2,4 -14,55 -25,112 -54,270 -139,504 -262,723 -28,51 -71,127 -95,169 -54,95 -275,380 -364,468 -36,36 -93,89 -128,118 -67,55 -96,63 -142,38 z"
-       id="path2"
-       sodipodi:nodetypes="cccccccsccccccccccscscccccccccccccscccccssccscccccccccccccccccscccccccccccccccccccscccccscccccsccsccccccscccscccccsccccssccccccccsscccccccccsscccsssscccccsscscccccccccccscscccccccsccccscsscccccccsscccccsscccssccccccc"
-       style="fill:#ffffff;fill-opacity:1" />
-    <path
-       d="m 2500.8313,4134.78 c -87.3956,-37.9843 -97.6852,-154.4873 -98,-194.7871 0,-44.3118 -4,-131.089 -9,-192.0177 -5,-60.9286 -5,-159.707 -2,-219.7125 6,-105.2404 7,-110.7794 34,-132.0121 79,-62.775 164,3.6926 217,168.9386 55,175.4007 49,403.4217 -15,519.7401 -35,64.6213 -71,78.4687 -127,49.8507 z"
-       id="path4"
-       sodipodi:nodetypes="ccsccccc"
-       style="stroke-width:1;fill:#ffffff;fill-opacity:1" />
-    <path
-       d="m 3344.4273,3996.6247 c -48.5583,-50.0953 -37.4868,-100.9153 -40.8178,-215.7915 -2.7697,-95.5212 29.3536,-310.9299 34.084,-408.9796 38.3413,-101.4275 148.4445,-115.5653 198.0212,-37.0586 31.0725,92.512 58.8307,202.0641 62.5807,328.6811 2.6164,88.3394 -12.9222,191.6407 -19.8758,275.1699 -22.7001,117.379 -144.8343,172.9775 -233.9923,57.9787 z"
-       id="path6"
-       sodipodi:nodetypes="csccscc"
-       style="stroke-width:1;fill:#ffffff;fill-opacity:1" />
-    <path
-       d="M4633 1040 c-60 -25 -78 -94 -158 -605 -20 -126 -45 -273 -56 -327 -10 -53 -19 -99 -19 -102 0 -3 41 -6 91 -6 85 0 90 1 84 19 -3 11 3 66 14 123 10 57 24 142 31 190 l12 87 46 -5 c111 -12 107 -10 141 -74 37 -68 81 -231 81 -296 l0 -44 85 0 85 0 -6 42 c-10 75 -44 237 -60 280 -8 22 -12 43 -9 46 2 2 46 -1 97 -7 181 -22 397 -31 756 -31 l362 0 0 80 0 80 -362 0 c-388 0 -565 9 -828 41 -183 21 -351 46 -357 52 -10 11 46 278 73 342 16 39 11 76 -14 98 -23 21 -62 28 -89 17z"
-       id="path8"
-       style="fill:#ffffff;fill-opacity:1" />
-  </g>
-</svg>
diff --git a/onlylegs/static/manifest.json b/onlylegs/static/manifest.json
deleted file mode 100644
index ab9009a..0000000
--- a/onlylegs/static/manifest.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-    "name": "OnlyLegs",
-    "short_name": "OnlyLegs",
-    "start_url": "/",
-    "display": "standalone",
-    "background_color": "#151515",
-    "theme_color": "#151515",
-    "description": "A gallery built for fast and simple image management!",
-    "icons": [
-        {
-            "src": "icon.png",
-            "sizes": "621x621",
-            "type": "image/png"
-        }
-    ],
-    "splash_pages": null
-  }
-  
\ No newline at end of file
diff --git a/onlylegs/static/sass/animations.sass b/onlylegs/static/sass/animations.sass
index 9fc623e..d85be1b 100644
--- a/onlylegs/static/sass/animations.sass
+++ b/onlylegs/static/sass/animations.sass
@@ -8,4 +8,12 @@
     0%
         left: -100%
     100%
-        left: 100%
\ No newline at end of file
+        left: 100%
+
+@keyframes imgFadeIn
+    0%
+        opacity: 0
+        // filter: blur(0.5rem)
+    100%
+        opacity: 1
+        // filter: blur(0)
diff --git a/onlylegs/static/sass/components/banner.sass b/onlylegs/static/sass/components/banner.sass
index 483c071..037e38d 100644
--- a/onlylegs/static/sass/components/banner.sass
+++ b/onlylegs/static/sass/components/banner.sass
@@ -20,26 +20,10 @@
             background-color: RGB($fg-black)
             color: RGB($fg-white)
 
-    &::after
-        content: ''
-
-        width: $rad
-        height: calc(#{$rad} * 2)
-
-        position: absolute
-        bottom: calc(#{$rad} * -2)
-        left: 0
-
-        background-color: RGB($bg-bright)
-        border-radius: $rad 0 0 0
-        box-shadow: 0 calc(#{$rad} * -1) 0 0 RGB($bg-100)
-
 .banner
     height: 30rem
     max-height: 69vh
 
-    background-color: RGB($bg-300)
-
     img
         position: absolute
         inset: 0
@@ -122,10 +106,10 @@
 
             background-color: RGB($primary)
             border-radius: $rad
+            overflow: hidden
 
 .banner-small
     height: 3.5rem
-    background-color: RGB($bg-100)
 
     .banner-content
         padding: 0 0.5rem
diff --git a/onlylegs/static/sass/components/buttons/block.sass b/onlylegs/static/sass/components/buttons/block.sass
index 3891edd..f16f96e 100644
--- a/onlylegs/static/sass/components/buttons/block.sass
+++ b/onlylegs/static/sass/components/buttons/block.sass
@@ -59,6 +59,10 @@
     &.black
         @include btn-block($black)
 
+    &.disabled, &:disabled
+        color: RGB($fg-dim)
+        cursor: unset
+
 .input-checkbox
     padding: 0
     display: flex
diff --git a/onlylegs/static/sass/components/buttons/pill.sass b/onlylegs/static/sass/components/buttons/pill.sass
index e9723f3..3767c01 100644
--- a/onlylegs/static/sass/components/buttons/pill.sass
+++ b/onlylegs/static/sass/components/buttons/pill.sass
@@ -68,6 +68,10 @@
 
         color: RGB($primary)
 
+    &.disabled, &:disabled
+        color: RGB($fg-dim)
+        cursor: unset
+
 .pill__critical
     color: RGB($critical)
 
diff --git a/onlylegs/static/sass/components/gallery.sass b/onlylegs/static/sass/components/gallery.sass
index 9b980ce..87d63ca 100644
--- a/onlylegs/static/sass/components/gallery.sass
+++ b/onlylegs/static/sass/components/gallery.sass
@@ -24,8 +24,6 @@
     margin: 0.35rem
     padding: 0
 
-    height: auto
-
     position: relative
 
     border-radius: $rad-inner
@@ -44,8 +42,7 @@
         height: auto
 
         position: absolute
-        left: 0
-        bottom: 0
+        inset: 0
 
         display: flex
         flex-direction: column
@@ -88,19 +85,6 @@
         object-position: center
 
         background-color: RGB($bg-bright)
-        filter: blur(0.5rem)
-        opacity: 0
-
-        transition: all 0.2s cubic-bezier(.79, .14, .15, .86)
-
-        &.loaded
-            filter: blur(0)
-            opacity: 1
-
-    &:after
-        content: ""
-        display: block
-        padding-bottom: 100%
 
     &:hover
         box-shadow: 0 0.2rem 0.4rem 0.1rem RGBA($bg-100, 0.6)
@@ -112,8 +96,6 @@
     margin: 0.35rem
     padding: 0
 
-    height: auto
-
     position: relative
 
     border-radius: $rad-inner
@@ -178,8 +160,7 @@
             height: 100%
 
             position: absolute
-            top: 0
-            left: 0
+            inset: 0
 
             object-fit: cover
             object-position: center
@@ -187,14 +168,8 @@
             background-color: RGB($bg-bright)
             border-radius: $rad-inner
             box-shadow: 0 0 0.4rem 0.25rem RGBA($bg-100, 0.1)
-            filter: blur(0.5rem)
-            opacity: 0
 
-            transition: all 0.2s cubic-bezier(.79, .14, .15, .86)
-
-            &.loaded
-                filter: blur(0)
-                opacity: 1
+            transition: transform 0.2s cubic-bezier(.79, .14, .15, .86)
 
         &.size-1
             .data-1
@@ -219,11 +194,6 @@
                 transform: scale(0.6) rotate(-1deg) translate(-15%, -23%)
                 z-index: +1
 
-    &:after
-        content: ""
-        display: block
-        padding-bottom: 100%
-
     &:hover
         .images
             &.size-1
@@ -252,3 +222,7 @@
 @media (max-width: 800px)
     .gallery-grid
         grid-template-columns: auto auto auto
+
+    .gallery-item
+        margin: 0.35rem
+        position: relative
\ No newline at end of file
diff --git a/onlylegs/static/sass/components/image-view.sass b/onlylegs/static/sass/components/image-view.sass
new file mode 100644
index 0000000..1439670
--- /dev/null
+++ b/onlylegs/static/sass/components/image-view.sass
@@ -0,0 +1,259 @@
+.info-container
+    padding: 0.5rem 0 0 0.5rem
+    width: 27rem
+    position: absolute
+    top: 0
+    left: 0
+    bottom: 0
+    background-image: linear-gradient(90deg, $bg-transparent, transparent)
+    overflow-y: auto
+    transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1)
+    z-index: 2
+    -ms-overflow-style: none
+    scrollbar-width: none
+    &::-webkit-scrollbar
+        display: none
+
+    &.collapsed
+        left: -27rem
+@media (max-width: 1100px)
+    .info-container
+        padding: 0 0.5rem 0 0.5rem
+        width: 100%
+        position: relative
+        background: none
+        // While probably not the best way of doing this
+        // Not bothered to fight with CSS today
+        &.collapsed
+            left: 0
+
+details
+    margin-bottom: 0.5rem
+    padding: 0.5rem
+    display: flex
+    flex-direction: column
+    background-color: RGB($bg-300)
+    color: RGB($fg-white)
+    border-radius: $rad
+    overflow: hidden
+
+    summary
+        height: 1.5rem
+        display: flex
+        flex-direction: row
+        justify-content: flex-start
+        align-items: center
+        position: relative
+        color: RGB($primary)
+
+        > i
+            margin-right: 0
+            font-size: 1.25rem
+
+            &.collapse-indicator
+                transition: transform 0.15s cubic-bezier(.79, .14, .15, .86)
+
+        h2
+            margin: 0 0.5rem
+            font-size: 1.1rem
+            font-weight: 500
+
+    &[open]
+        summary
+            margin-bottom: 0.5rem
+
+            > i.collapse-indicator
+                transform: rotate(90deg)
+
+    p
+        margin: 0
+        padding: 0
+
+        font-size: 1rem
+        font-weight: 400
+
+        text-overflow: ellipsis
+        overflow: hidden
+
+    .link
+        margin: 0
+        padding: 0
+
+        color: RGB($primary)
+
+        cursor: pointer
+        text-decoration: none
+
+        &:hover
+            text-decoration: underline
+
+    .pfp
+        width: 1.1rem
+        height: 1.1rem
+
+        border-radius: $rad-inner
+
+        object-fit: cover
+
+    table
+        margin: 0
+        padding: 0
+
+        width: 100%
+
+        overflow-x: hidden
+        border-collapse: collapse
+
+        tr
+            white-space: nowrap
+
+            td
+                padding-bottom: 0.5rem
+
+                max-width: 0
+
+                font-size: 1rem
+                font-weight: 400
+
+                vertical-align: top
+
+                > *
+                    vertical-align: top
+
+            td:first-child
+                padding-right: 0.5rem
+
+                width: 50%
+
+                overflow: hidden
+                text-overflow: ellipsis
+                white-space: nowrap
+            td:last-child
+                width: 50%
+
+                white-space: normal
+                word-break: break-word
+
+        tr:last-of-type td
+            padding-bottom: 0
+
+.img-colours
+    width: 100%
+
+    display: flex
+    gap: 0.5rem
+
+    button
+        margin: 0
+        padding: 0
+
+        width: 1.6rem
+        height: 1.6rem
+
+        display: flex
+        justify-content: center
+        align-items: center
+
+        border-radius: $rad-inner
+        border: none
+
+        i
+            font-size: 1rem
+            opacity: 0
+            transition: opacity 0.15s ease-in-out
+
+        &:hover i
+            opacity: 1
+
+.img-groups
+    width: 100%
+    display: flex
+    flex-wrap: wrap
+    gap: 0.5rem
+
+.image-container
+    padding: 0.5rem
+    position: absolute
+    top: 0
+    left: 27rem
+    right: 0
+    bottom: 0
+    z-index: 2
+    transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1), padding 0.3s cubic-bezier(0.76, 0, 0.17, 1)
+
+    picture
+        margin: auto
+        width: 100%
+        height: 100%
+        display: flex
+        overflow: hidden
+
+        img
+            margin: auto
+            padding: 0
+            width: auto
+            height: auto
+            max-width: 100%
+            max-height: 100%
+            object-fit: contain
+            object-position: center
+            border-radius: $rad
+
+    &.collapsed
+        padding: 0
+        left: 0
+
+        picture img
+            border-radius: 0
+@media (max-width: 1100px)
+    .image-container
+        position: relative
+        left: 0
+
+        picture
+            margin: 0 auto
+            max-height: 69vh
+
+            img
+                max-height: 69vh
+
+        &.collapsed
+            padding: 0.5rem
+            left: 0
+
+            picture img
+                border-radius: $rad
+
+.background
+    position: absolute
+    inset: 0
+    background-color: RGB($bg-300)
+    background-image: linear-gradient(to right, RGB($bg-400) 15%, RGB($bg-200) 35%, RGB($bg-400) 50%)
+    background-size: 1000px 640px
+    animation: imgLoading 1.8s linear infinite forwards
+    user-select: none
+    overflow: hidden
+    z-index: 1
+
+    img
+        position: absolute
+        inset: 0
+        width: 100%
+        height: 100%
+        background-color: RGB($fg-white)
+        filter: blur(3rem) saturate(1.2) brightness(0.7)
+        transform: scale(1.1)
+        object-fit: cover
+        object-position: center center
+
+    &::after
+        content: ''
+        position: absolute
+        inset: 0
+        width: 100%
+        height: 100%
+        z-index: +1
+
+@media (max-width: 1100px)
+    #fullscreenImage
+        display: none
\ No newline at end of file
diff --git a/onlylegs/static/sass/components/image-view/background.sass b/onlylegs/static/sass/components/image-view/background.sass
deleted file mode 100644
index e7478b5..0000000
--- a/onlylegs/static/sass/components/image-view/background.sass
+++ /dev/null
@@ -1,42 +0,0 @@
-.background
-    width: 100%
-    height: 100vh
-
-    position: fixed
-    top: 0
-    left: 0
-
-    background-color: RGB($bg-300)
-    background-image: linear-gradient(to right, RGB($bg-400) 15%, RGB($bg-200) 35%, RGB($bg-400) 50%)
-    background-size: 1000px 640px
-    animation: imgLoading 1.8s linear infinite forwards
-
-    user-select: none
-    overflow: hidden
-    z-index: 1
-
-    img
-        position: absolute
-        top: 0
-        left: 0
-
-        width: 100%
-        height: 100%
-
-        background-color: RGB($fg-white)
-
-        filter: blur(1rem) saturate(1.2)
-        transform: scale(1.1)
-
-        object-fit: cover
-        object-position: center center
-
-    span
-        position: absolute
-        top: 0
-        left: 0
-
-        width: 100%
-        height: 100%
-
-        z-index: +1
\ No newline at end of file
diff --git a/onlylegs/static/sass/components/image-view/image.sass b/onlylegs/static/sass/components/image-view/image.sass
deleted file mode 100644
index 99fd1ac..0000000
--- a/onlylegs/static/sass/components/image-view/image.sass
+++ /dev/null
@@ -1,28 +0,0 @@
-.image-container
-    margin: auto
-
-    width: 100%
-    height: 100%
-
-    display: flex
-    overflow: hidden
-
-    img
-        margin: auto
-        padding: 0
-
-        width: auto
-        height: auto
-        max-width: 100%
-        max-height: 100%
-
-        object-fit: contain
-        object-position: center
-
-@media (max-width: 1100px)
-    .image-container
-        margin: 0 auto
-        max-height: 69vh
-
-        img
-            max-height: 69vh
\ No newline at end of file
diff --git a/onlylegs/static/sass/components/image-view/info-tab.sass b/onlylegs/static/sass/components/image-view/info-tab.sass
deleted file mode 100644
index 1b42c1e..0000000
--- a/onlylegs/static/sass/components/image-view/info-tab.sass
+++ /dev/null
@@ -1,215 +0,0 @@
-.info-container
-    padding: 0.5rem 0 0.5rem 0.5rem
-
-    width: 27rem
-    height: 100vh
-
-    position: absolute
-    top: 0
-    left: 0
-
-    display: flex
-    flex-direction: column
-    gap: 0.5rem
-
-    background-image: linear-gradient(90deg, $bg-transparent, transparent)
-
-    overflow-y: auto
-    z-index: +4
-    transition: left 0.3s cubic-bezier(0.76, 0, 0.17, 1)
-    -ms-overflow-style: none
-    scrollbar-width: none
-    &::-webkit-scrollbar
-        display: none
-
-    &.collapsed
-        left: -27rem
-
-.info-tab
-    width: 100%
-
-    display: flex
-    flex-direction: column
-
-    position: relative
-
-    background-color: RGB($bg-300)
-    border-radius: $rad
-
-    transition: max-height 0.3s cubic-bezier(.79, .14, .15, .86)
-
-    &.collapsed
-        height: 2.5rem
-
-        .collapse-indicator
-            transform: rotate(90deg)
-
-        .info-header
-            border-radius: $rad
-
-        .info-table
-            display: none
-
-.collapse-indicator
-    margin: 0
-    padding: 0
-    
-    position: absolute
-    top: 0.6rem
-    right: 0.6rem
-
-    background-color: transparent
-    color: RGB($primary)
-    border: none
-
-    z-index: +2
-
-    transition: transform 0.15s cubic-bezier(.79, .14, .15, .86)
-    cursor: pointer
-
-    > i
-        font-size: 1.1rem
-        color: RGB($primary)
-
-.info-header
-    padding: 0.5rem
-
-    width: 100%
-    height: 2.5rem
-
-    display: flex
-    justify-content: start
-    align-items: center
-    gap: 0.5rem
-
-    background-color: RGB($bg-200)
-    border-radius: $rad $rad 0 0
-
-    > i
-        font-size: 1.25rem
-        color: RGB($primary)
-
-    h2
-        margin: 0
-
-        font-size: 1.1rem
-        font-weight: 500
-
-        color: RGB($primary)
-
-        text-overflow: ellipsis
-        overflow: hidden
-
-.info-table
-    margin: 0
-    padding: 0.5rem
-
-    display: flex
-    flex-direction: column
-    gap: 1rem
-
-    color: RGB($fg-white)
-
-    overflow-x: hidden
-
-    p
-        margin: 0
-        padding: 0
-
-        font-size: 1rem
-        font-weight: 400
-
-        text-overflow: ellipsis
-        overflow: hidden
-
-    .link
-        margin: 0
-        padding: 0
-
-        color: RGB($primary)
-
-        cursor: pointer
-        text-decoration: none
-
-        &:hover
-            text-decoration: underline
-
-    table
-        margin: 0
-        padding: 0
-
-        width: 100%
-
-        overflow-x: hidden
-        border-collapse: collapse
-
-        tr
-            white-space: nowrap
-
-            td
-                padding-bottom: 0.5rem
-
-                max-width: 0
-
-                font-size: 1rem
-                font-weight: 400
-
-                vertical-align: top
-
-            td:first-child
-                padding-right: 0.5rem
-
-                width: 50%
-
-                overflow: hidden
-                text-overflow: ellipsis
-                white-space: nowrap
-            td:last-child
-                width: 50%
-
-                white-space: normal
-                word-break: break-word
-
-        tr:last-of-type td
-            padding-bottom: 0
-
-.img-colours
-    width: 100%
-
-    display: flex
-    gap: 0.5rem
-
-    span
-        margin: 0
-        padding: 0
-
-        width: 1.5rem
-        height: 1.5rem
-
-        display: flex
-        justify-content: center
-        align-items: center
-
-        border-radius: $rad-inner
-        // border: 1px solid RGB($white)
-
-.img-groups
-    width: 100%
-    display: flex
-    flex-wrap: wrap
-    gap: 0.5rem
-
-@media (max-width: 1100px)
-    .info-container
-        padding: 0.5rem
-
-        width: 100%
-        height: 100%
-
-        position: relative       
-
-        &.collapsed
-            left: unset
-
-    .info-container
-        background: transparent
diff --git a/onlylegs/static/sass/components/image-view/view.sass b/onlylegs/static/sass/components/image-view/view.sass
deleted file mode 100644
index 8103d88..0000000
--- a/onlylegs/static/sass/components/image-view/view.sass
+++ /dev/null
@@ -1,59 +0,0 @@
-@import 'background'
-@import 'info-tab'
-@import 'image'
-
-
-.image-grid
-    padding: 0
-
-    width: 100%
-    height: 100vh
-
-    position: relative
-
-    display: flex
-    flex-direction: column
-    z-index: 3
-
-    .image-block
-        margin: 0 0 0 27rem
-        padding: 0.5rem
-
-        width: calc(100% - 27rem)
-        height: 100vh
-
-        position: relative
-
-        display: flex
-        flex-direction: column
-        gap: 0
-
-        z-index: 3
-        transition: margin 0.3s cubic-bezier(0.76, 0, 0.17, 1), width 0.3s cubic-bezier(0.76, 0, 0.17, 1)
-
-        .pill-row
-            margin-top: 0.5rem
-
-    &.collapsed
-        .image-block
-            margin: 0
-            width: 100%
-
-@media (max-width: 1100px)
-    .image-grid
-        height: auto
-
-        .image-block
-            margin: 0
-            padding: 0.5rem 0.5rem 0 0.5rem
-
-            width: 100%
-            height: auto
-
-            transition: margin 0s, width 0s
-
-            .pill-row
-                #fullscreenImage
-                    display: none
-
-        
diff --git a/onlylegs/static/sass/components/navigation.sass b/onlylegs/static/sass/components/navigation.sass
index c58805e..760a7d3 100644
--- a/onlylegs/static/sass/components/navigation.sass
+++ b/onlylegs/static/sass/components/navigation.sass
@@ -1,36 +1,20 @@
-.navigation
-    margin: 0
-    padding: 0
-
+nav
     width: 3.5rem
     height: 100%
     height: 100dvh
 
     display: flex
     flex-direction: column
-    justify-content: space-between
 
     position: fixed
     top: 0
     left: 0
 
-    background-color: RGB($bg-100)
-    color: RGB($fg-white)
+    background-color: transparent
+    color: inherit
 
     z-index: 69
 
-    .logo
-        margin: 0
-        padding: 0
-
-        width: 3.5rem
-        height: 3.5rem
-        min-height: 3.5rem
-
-        display: flex
-        flex-direction: row
-        align-items: center
-
 .navigation-spacer
     height: 100%
 
@@ -50,6 +34,7 @@
     align-items: center
 
     background-color: transparent
+    color: inherit
     border: none
 
     text-decoration: none
@@ -58,7 +43,7 @@
         padding: 0.5rem
         font-size: 1.3rem
         border-radius: $rad-inner
-        color: RGB($fg-white)
+        color: inherit
 
     > .nav-pfp
         padding: 0.4rem
@@ -72,68 +57,29 @@
             object-fit: cover
             border-radius: $rad-inner
 
-    .tool-tip
-        padding: 0.4rem 0.7rem
-
-        display: block
-
-        position: absolute
-        top: 50%
-        left: 3rem
-        transform: translateY(-50%)
-
-        font-size: 0.9rem
-        font-weight: 500
-
-        background-color: RGB($bg-100)
-        color: RGB($fg-white)
-        opacity: 0
-        border-radius: $rad-inner
-
-        transition: opacity 0.2s cubic-bezier(.76,0,.17,1), left 0.2s cubic-bezier(.76,0,.17,1)
-
-        pointer-events: none
-
-        > i
-            display: block
-
-            position: absolute
-            top: 50%
-            left: -0.45rem
-            transform: translateY(-50%)
-            
-            font-size: 0.75rem
-
-            color: RGB($bg-100)
-
     &:hover
         > i, .nav-pfp
             background: RGBA($fg-white, 0.1)
 
-        span
-            opacity: 1
-            left: 3.9rem
-
     &.selected
-        > i
-            color: RGB($primary)
+        color: RGB($primary)
 
         &::before
             content: ''
             display: block
 
             position: absolute
-            top: 3px
-            left: 0
+            top: 0.5rem
+            left: 0.2rem
 
             width: 3px
-            height: calc(100% - 6px)
+            height: calc(100% - 1rem)
 
-            background-color: RGB($primary)
+            background-color: currentColor
             border-radius: $rad-inner
 
 @media (max-width: $breakpoint)
-    .navigation
+    nav
         width: 100vw
         height: 3.5rem
     
@@ -145,6 +91,8 @@
         bottom: 0
         left: 0
 
+        background-color: RGB($background)
+
         > span
             display: none
 
diff --git a/onlylegs/static/sass/components/notification.sass b/onlylegs/static/sass/components/notification.sass
index 3a468a2..feea589 100644
--- a/onlylegs/static/sass/components/notification.sass
+++ b/onlylegs/static/sass/components/notification.sass
@@ -22,23 +22,22 @@
     margin: 0
     padding: 0
 
-    width: 450px
+    width: 24rem
     height: auto
 
     position: fixed
-    top: 0.3rem
+    bottom: 0.3rem
     right: 0.3rem
 
     display: flex
-    flex-direction: column
+    flex-direction: column-reverse
 
     z-index: 621
 
 .sniffle__notification
-    margin: 0 0 0.3rem 0
+    margin-top: 0.3rem
     padding: 0
 
-    width: 450px
     height: auto
     max-height: 100px
 
@@ -56,7 +55,7 @@
     box-sizing: border-box
     overflow: hidden
 
-    transition: all 0.25s ease-in-out, opacity 0.2s ease-in-out, transform 0.2s cubic-bezier(.68,-0.55,.27,1.55)
+    transition: opacity 0.2s ease-in-out, transform 0.2s cubic-bezier(.68,-0.55,.27,1.55)
 
     &::after
         content: ""
@@ -89,10 +88,8 @@
     &.hide
         margin: 0
         max-height: 0
-
         opacity: 0
         transform: translateX(100%)
-
         transition: all 0.4s ease-in-out, max-height 0.2s ease-in-out
 
 .sniffle__notification-icon
@@ -130,6 +127,7 @@
 
 @media (max-width: $breakpoint)
     .notifications
+        bottom: 3.8rem
         width: calc(100vw - 0.6rem)
         height: auto
 
@@ -138,7 +136,7 @@
 
         &.hide
             opacity: 0
-            transform: translateY(-5rem)
+            transform: translateY(1rem)
     
     .sniffle__notification-time
         width: 100%
diff --git a/onlylegs/static/sass/components/settings.sass b/onlylegs/static/sass/components/settings.sass
index cce4ac3..8587a76 100644
--- a/onlylegs/static/sass/components/settings.sass
+++ b/onlylegs/static/sass/components/settings.sass
@@ -26,4 +26,4 @@
         padding: 0
 
         font-size: 1.25rem
-        font-weight: 700
\ No newline at end of file
+        font-weight: 700
diff --git a/onlylegs/static/sass/components/tags.sass b/onlylegs/static/sass/components/tags.sass
index 4901829..6da6e67 100644
--- a/onlylegs/static/sass/components/tags.sass
+++ b/onlylegs/static/sass/components/tags.sass
@@ -1,11 +1,11 @@
 .tag-icon
     margin: 0
-    padding: 0.25rem 0.5rem
+    padding: 0.3rem 0.5rem
 
     display: flex
-    align-items: center
+    align-items: flex-end
     justify-content: center
-    gap: 0.25rem
+    gap: 0.3rem
 
     font-size: 0.9rem
     font-weight: 500
@@ -19,9 +19,8 @@
     cursor: pointer
     transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
 
-    svg
-        width: 1.15rem
-        height: 1.15rem
+    i
+        font-size: 1.15rem
 
     &:hover
-        background-color: RGBA($primary, 0.3)
+        background-color: RGBA($primary, 0.2)
diff --git a/onlylegs/static/sass/style.sass b/onlylegs/static/sass/style.sass
index 097346c..daf7094 100644
--- a/onlylegs/static/sass/style.sass
+++ b/onlylegs/static/sass/style.sass
@@ -18,15 +18,13 @@
 @import "components/buttons/pill"
 @import "components/buttons/block"
 
-@import "components/image-view/view"
+@import "components/image-view"
 @import "components/settings"
 
-// Reset
 *
     box-sizing: border-box
-    font-family: $font
-
     scrollbar-color: RGB($primary) transparent
+    font-family: $font
 
     ::-webkit-scrollbar
         width: 0.5rem
@@ -37,66 +35,49 @@
     ::-webkit-scrollbar-thumb:hover
         background: RGB($fg-white)
 
-html, body
+html
     margin: 0
     padding: 0
-
-    min-height: 100vh
-    max-width: 100vw
-
-    background-color: RGB($fg-white)
-
     scroll-behavior: smooth
-    overflow-x: hidden
 
-.wrapper
+body
     margin: 0
     padding: 0 0 0 3.5rem
 
+    max-width: 100%
     min-height: 100vh
 
+    display: grid
+    grid-template-rows: auto 1fr auto
+
+    background-color: RGB($background)
+    color: RGB($foreground)
+
+    overflow-x: hidden
+@media (max-width: $breakpoint)
+    body
+        padding: 0 0 3.5rem 0
+
+main
     display: flex
     flex-direction: column
-
-    background-color: RGB($bg-bright)
-    color: RGB($bg-100)
-
-.big-text
-    height: 20rem
-
-    display: flex
-    flex-direction: column
-    justify-content: center
-    align-items: center
-
-    color: RGB($bg-100)
-
-    h1
-        margin: 0 2rem
-        
-        font-size: 4rem
-        font-weight: 900
-        text-align: center
-
-    p
-        margin: 0 2rem
-
-        max-width: 40rem
-        font-size: 1rem
-        font-weight: 400
-        text-align: center
+    position: relative
+    background: RGBA($white, 1)
+    color: RGB($fg-black)
+    border-top-left-radius: $rad
+    overflow: hidden
+@media (max-width: $breakpoint)
+    main
+        border-top-left-radius: 0
 
 .error-page
-    width: 100%
-    height: 100vh
+    min-height: 100%
 
     display: flex
     flex-direction: column
     justify-content: center
     align-items: center
 
-    background-color: RGB($bg-bright)
-
     h1
         margin: 0 2rem
         
@@ -113,23 +94,8 @@ html, body
         font-size: 1.25rem
         font-weight: 400
         text-align: center
-
-        color: $fg-black
-
-
 @media (max-width: $breakpoint)
-    .wrapper
-        padding: 0 0 3.5rem 0
-
-    .big-text
-        height: calc(75vh - 3.5rem)
-
-        h1
-            font-size: 3.5rem
-
     .error-page
-        height: calc(100vh - 3.5rem)
-
         h1
             font-size: 4.5rem
 
diff --git a/onlylegs/static/sass/variables.sass b/onlylegs/static/sass/variables.sass
index bc1bd9f..e3fcd83 100644
--- a/onlylegs/static/sass/variables.sass
+++ b/onlylegs/static/sass/variables.sass
@@ -37,6 +37,11 @@ $font: 'Rubik', sans-serif
 $breakpoint: 800px
 
 
+// New variables, Slowly moving over to them because I suck at planning ahead and coding reeee
+$background: var(--bg-100)
+$foreground: var(--fg-white)
+
+
 \:root
     --bg-dim: 16, 16, 16
     --bg-bright: 232, 227, 227
@@ -66,7 +71,7 @@ $breakpoint: 800px
     --success: var(--green)
     --info: var(--blue)
 
-    --rad: 8px
+    --rad: 0.5rem
     --rad-inner: calc(var(--rad) / 2)
 
     --animation-smooth: cubic-bezier(0.76, 0, 0.17, 1)
diff --git a/onlylegs/templates/base.html b/onlylegs/templates/base.html
new file mode 100644
index 0000000..b580c7f
--- /dev/null
+++ b/onlylegs/templates/base.html
@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <title>{{ config.WEBSITE_CONF.name }}</title>
+
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+    <meta name="description" content="{{ config.WEBSITE_CONF.motto }}">
+    <meta name="author" content="{{ config.WEBSITE_CONF.author }}">
+
+    <meta property="og:title" content="{{ config.WEBSITE_CONF.name }}">
+    <meta property="og:description" content="{{ config.WEBSITE_CONF.motto }}">
+    <meta property="og:type" content="website">
+
+    <meta name="twitter:title" content="{{ config.WEBSITE_CONF.name }}">
+    <meta name="twitter:description" content="{{ config.WEBSITE_CONF.motto }}">
+
+    <!-- Fonts -->
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap">
+
+    <!-- phosphor icons -->
+    <script src="https://unpkg.com/@phosphor-icons/web"></script>
+    
+    <!-- Favicon -->
+    <link rel="icon" href="{{url_for('static', filename='icon.png')}}" type="image/png">
+
+    {% assets "scripts" %} <script type="text/javascript" src="{{ ASSET_URL }}"></script> {% endassets %}
+    {% assets "styles" %} <link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css" defer> {% endassets %}
+    {% block head %}{% endblock %}
+</head>
+<body>
+    <div class="notifications"></div>
+    <button class="top-of-page" aria-label="Jump to top of page"><i class="ph ph-arrow-up"></i></button>
+
+    <div class="pop-up">
+        <span class="pop-up__click-off" onclick="popupDismiss()"></span>
+        <div class="pop-up-wrapper">
+            <div class="pop-up-header"></div>
+            <div class="pop-up-controlls"></div>
+        </div>
+    </div>
+
+    <nav>
+        <a href="{{ url_for('gallery.index') }}{% block page_index %}{% endblock %}" class="navigation-item {% block nav_home %}{% endblock %}" aria-label="Home Page">
+            <i class="ph-fill ph-images-square"></i>
+        </a>
+
+        <a href="{{ url_for('group.groups') }}" class="navigation-item {% block nav_groups %}{% endblock %}" aria-label="Photo Groups">
+            <i class="ph-fill ph-package"></i>
+        </a>
+
+        {% if current_user.is_authenticated %}
+            <button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()" aria-label="Upload Photo">
+                <i class="ph-fill ph-upload"></i>
+            </button>
+        {% endif %}
+
+        <span class="navigation-spacer"></span>
+
+        {% if current_user.is_authenticated %}
+            <a href="{{ url_for('profile.profile') }}" class="navigation-item {% block nav_profile %}{% endblock %}" aria-label="Profile Page">
+                {% if current_user.picture %}
+                    <span class="nav-pfp">
+                        <picture>
+                            <source srcset="{{ url_for('api.media', path='pfp/' + current_user.picture) }}?r=pfp&e=webp">
+                            <source srcset="{{ url_for('api.media', path='pfp/' + current_user.picture) }}?r=pfp&e=png">
+                            <img
+                                src="{{ url_for('api.media', path='pfp/' + current_user.picture) }}?r=icon"
+                                alt="Profile picture"
+                                onload="imgFade(this)"
+                                style="opacity:0;"
+                            >
+                        </picture>
+                    </span>
+                {% else %}
+                    <i class="ph-fill ph-folder-simple-user"></i>
+                {% endif %}
+            </a>
+
+            <a href="{{ url_for('settings.general') }}" class="navigation-item {% block nav_settings %}{% endblock %}" aria-label="Gallery Settings">
+                <i class="ph-fill ph-gear-fine"></i>
+            </a>
+        {% else %}
+            <button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()" aria-label="Sign up or Login">
+                <i class="ph-fill ph-sign-in"></i>
+            </button>
+        {% endif %}
+    </nav>
+
+    <header>{% block header %}{% endblock %}</header>
+
+    <main>
+        {% if current_user.is_authenticated %}
+            <div class="upload-panel">
+                <span class="click-off" onclick="closeUploadTab()"></span>
+                <div class="container">
+                    <span id="dragIndicator"></span>
+                    <h3>Upload stuffs</h3>
+                    <p>May the world see your stuff 👀</p>
+                    <form id="uploadForm">
+                        <button class="fileDrop-block" type="button">
+                            <i class="ph ph-upload"></i>
+                            <span class="status">Choose or Drop file</span>
+                            <input type="file" id="file" tab-index="-1">
+                        </button>
+
+                        <input class="input-block" type="text" placeholder="alt" id="alt">
+                        <input class="input-block" type="text" placeholder="description" id="description">
+                        <input class="input-block" type="text" placeholder="tags" id="tags">
+                        <button class="btn-block primary" type="submit">Upload</button>
+                    </form>
+                    <div class="upload-jobs"></div>
+                </div>
+            </div>
+        {% endif %}
+
+        {% block content %}{% endblock %}
+    </main>
+
+    <script type="text/javascript">
+        keepSquare();
+
+        const times = document.querySelectorAll('.time');
+        for (let i = 0; i < times.length; i++) {
+            // Remove milliseconds
+            const raw = times[i].innerHTML.split('.')[0];
+    
+            // Parse YYYY-MM-DD HH:MM:SS to Date object
+            const time = raw.split(' ')[1];
+            const date = raw.split(' ')[0].split('-');
+    
+            // Format to YYYY/MM/DD HH:MM:SS and convert to UTC Date object
+            const dateTime = new Date(`${date[0]}/${date[1]}/${date[2]} ${time} UTC`);
+    
+            // Convert to local time
+            times[i].innerHTML = `${dateTime.toLocaleDateString()} ${dateTime.toLocaleTimeString()}`;
+        }
+    
+        // Top Of Page button
+        const topOfPage = document.querySelector('.top-of-page');
+        if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
+            topOfPage.classList.add('show');
+        } else {
+            topOfPage.classList.remove('show');
+        }
+        topOfPage.onclick = () => {
+            document.body.scrollTop = 0;
+            document.documentElement.scrollTop = 0;
+        }
+    
+        // Info button
+        const infoButton = document.querySelector('.info-button');
+        if (infoButton) {
+            if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
+                infoButton.classList.remove('show');
+            } else {
+                infoButton.classList.add('show');
+            }
+            infoButton.onclick = () => {
+                popupShow('OnlyLegs',
+                            '<a href="https://github.com/Fluffy-Bean/onlylegs">v{{ config['APP_VERSION'] }}</a> ' +
+                            'using <a href="https://phosphoricons.com/">Phosphoricons</a> and Flask.' +
+                            '<br>Made by Fluffy and others with ❤️');
+            }
+        }
+
+        window.onload = () => { keepSquare(); }
+        window.onresize = () => { keepSquare(); }
+        window.onscroll = () => {
+            if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
+                topOfPage.classList.add('show');
+            } else {
+                topOfPage.classList.remove('show');
+            }
+        
+            if (infoButton) {
+                if (document.body.scrollTop > 300 || document.documentElement.scrollTop > 20) {
+                    infoButton.classList.remove('show');
+                } else {
+                    infoButton.classList.add('show');
+                }
+            }
+        }
+
+        {% for message in get_flashed_messages() %}
+            addNotification('{{ message[0] }}', {{ message[1] }});
+        {% endfor %}
+    </script>
+
+    {% block script %}{% endblock %}
+</body>
+</html>
\ No newline at end of file
diff --git a/onlylegs/templates/error.html b/onlylegs/templates/error.html
index 9d76c79..7e9c20c 100644
--- a/onlylegs/templates/error.html
+++ b/onlylegs/templates/error.html
@@ -1,7 +1,7 @@
-{% extends 'layout.html' %}
+{% extends 'base.html' %}
 {% block content %}
-    <span class="error-page">
+    <div class="error-page">
         <h1>{{error}}</h1>
         <p>{{msg}}</p>
-    </span>
+    </div>
 {% endblock %}
diff --git a/onlylegs/templates/group.html b/onlylegs/templates/group.html
index bebe8ec..67cfb1f 100644
--- a/onlylegs/templates/group.html
+++ b/onlylegs/templates/group.html
@@ -1,227 +1,76 @@
-{% extends 'layout.html' %}
+{% extends 'base.html' %}
+{% from 'macros/image.html' import gallery_item %}
 {% block nav_groups %}selected{% endblock %}
+
 {% block head %}
     {% if images %}
-        <meta name="theme-color" content="rgb({{ images.0.colours.0.0 }}{{ images.0.colours.0.1 }}{{ images.0.colours.0.2 }})"/>
+        <meta property="og:image" content="{{ url_for('api.media', path='uploads/' + images.0.filename) }}"/>
+        <meta name="twitter:image" content="{{ url_for('api.media', path='uploads/' + images.0.filename) }}">
+        <meta name="theme-color" content="rgb{{ images.0.colours.0 }}"/>
+        <meta name="twitter:card" content="summary_large_image">
     {% endif %}
 
     <script type="text/javascript">
-        function groupShare() {
-            try {
-                navigator.clipboard.writeText(window.location.href)
-                addNotification("Copied link!", 4);
-            } catch (err) {
-                addNotification("Failed to copy link! Are you on HTTP?", 2);
-            }
+        group_data = {
+            'id': {{ group.id }},
+            'name': "{{ group.name }}",
+            'description': "{{ group.description }}",
         }
-
-        {% if current_user.id == group.author.id %}
-            function groupDelete() {
-                cancelBtn = document.createElement('button');
-                cancelBtn.classList.add('btn-block');
-                cancelBtn.classList.add('transparent');
-                cancelBtn.innerHTML = 'AAAAAAAAAA';
-                cancelBtn.onclick = popupDissmiss;
-
-                deleteBtn = document.createElement('button');
-                deleteBtn.classList.add('btn-block');
-                deleteBtn.classList.add('critical');
-                deleteBtn.innerHTML = 'No ragrats!';
-                deleteBtn.onclick = deleteConfirm;
-                
-                popUpShow('Yeet!',
-                          'Are you surrrre? This action is irreversible and very final.' +
-                          ' This wont delete the images, but it will remove them from this group.',
-                          null,
-                          [cancelBtn, deleteBtn]);
-            }
-
-            function deleteConfirm(event) {
-                // AJAX takes control of subby form :3
-                event.preventDefault();
-            
-                let formID = {{ group.id }};
-            
-                if (!formID) {
-                    addNotification("Dont tamper with the JavaScript pls!", 3);
-                    return;
-                }
-            
-                // Make form
-                const formData = new FormData();
-                formData.append("group", formID);
-            
-                fetch('{{ url_for('group_api.delete_group') }}', {
-                    method: 'POST',
-                    body: formData
-                }).then(response => {
-                    if (response.status === 200) {
-                        // Redirect to groups page
-                        window.location.href = '{{ url_for('group.groups') }}';
-                    } else {
-                        switch (response.status) {
-                            case 500:
-                                addNotification('Server exploded, F\'s in chat', 2);
-                                break;
-                            case 403:
-                                addNotification('None but devils play past here... Bad information', 2);
-                                break;
-                            default:
-                                addNotification('Error logging in, blame someone', 2);
-                                break;
-                        }
-                    }
-                }).catch(error => {
-                    addNotification('Error yeeting group!', 2);
-                });
-            }
-
-            function groupEdit() {
-                // Create elements
-                cancelBtn = document.createElement('button');
-                cancelBtn.classList.add('btn-block');
-                cancelBtn.classList.add('transparent');
-                cancelBtn.innerHTML = 'go baaaaack';
-                cancelBtn.onclick = popupDissmiss;
-
-                submitBtn = document.createElement('button');
-                submitBtn.classList.add('btn-block');
-                submitBtn.classList.add('primary');
-                submitBtn.innerHTML = 'Saveeee';
-                submitBtn.type = 'submit';
-                submitBtn.setAttribute('form', 'editForm');
-
-                // Create form
-                editForm = document.createElement('form');
-                editForm.id = 'editForm';
-                editForm.setAttribute('onsubmit', 'return edit(event);');
-
-                groupInput = document.createElement('input');
-                groupInput.classList.add('input-block');
-                groupInput.type = 'text';
-                groupInput.placeholder = 'Group ID';
-                groupInput.value = {{ group.id }};
-                groupInput.id = 'group';
-
-                imageInput = document.createElement('input');
-                imageInput.classList.add('input-block');
-                imageInput.type = 'text';
-                imageInput.placeholder = 'Image ID';
-                imageInput.id = 'image';
-
-                actionInput = document.createElement('input');
-                actionInput.classList.add('input-block');
-                actionInput.type = 'text';
-                actionInput.placeholder = 'add/remove';
-                actionInput.value = 'add';
-                actionInput.id = 'action';
-
-                editForm.appendChild(groupInput);
-                editForm.appendChild(imageInput);
-                editForm.appendChild(actionInput);
-
-                popUpShow(
-                    'Nothing stays the same',
-                    'Add, remove, or change, the power is in your hands...',
-                    editForm,
-                    [cancelBtn, submitBtn]
-                );
-            }
-
-            function edit(event) {
-                // AJAX takes control of subby form :3
-                event.preventDefault();
-            
-                let formGroup = document.querySelector("#group").value;
-                let formImage = document.querySelector("#image").value;
-                let formAction = document.querySelector("#action").value;
-            
-                if (!formGroup || !formImage || !formAction) {
-                    addNotification("All values must be set!", 3);
-                    return;
-                }
-            
-                // Make form
-                const formData = new FormData();
-                formData.append("group", formGroup);
-                formData.append("image", formImage);
-                formData.append("action", formAction);
-            
-                fetch('{{ url_for('group_api.modify_group') }}', {
-                    method: 'POST',
-                    body: formData
-                }).then(response => {
-                    if (response.status === 200) {
-                        addNotification('Group edited!!!', 1);
-                        popupDissmiss();
-                    } else {
-                        switch (response.status) {
-                            case 500:
-                                addNotification('Server exploded, F\'s in chat', 2);
-                                break;
-                            case 403:
-                                addNotification('None but devils play past here... Bad information', 2);
-                                break;
-                            default:
-                                addNotification('Error logging in, blame someone', 2);
-                                break;
-                        }
-                    }
-                }).catch(error => {
-                    addNotification('Error!!!!! Panic!!!!', 2);
-                });
-            }
-        {% endif %}
     </script>
 
     <style>
         {% if images %}
-            .banner::after {
-                box-shadow: 0 calc(var(--rad) * -1) 0 0 rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }});
-            }
-            .banner-content p {
+            :root { --bg-100: {{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }} }
+
+            body {
+                background: rgb{{ images.0.colours.0 }} !important;
                 color: {{ text_colour }} !important;
             }
-            .banner-content h1 {
-                color: {{ text_colour }} !important;
+            main {
+                background: rgba(var(--white), 0.6) !important;
             }
 
+
+            .navigation-item.selected { color: {{ text_colour }} !important; }
+
+            .banner .banner-content .banner-header,
+            .banner .banner-content .banner-info,
+            .banner .banner-content .banner-subtitle {
+                color: {{ text_colour }} !important;
+            }
             .banner-content .link {
                 background-color: {{ text_colour }} !important;
-                color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important;
+                color: rgb{{ images.0.colours.0 }} !important;
             }
             .banner-content .link:hover {
-                background-color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important;
+                background-color: rgb{{ images.0.colours.0 }} !important;
                 color: {{ text_colour }} !important;
             }
 
             .banner-filter {
-                background: linear-gradient(90deg, rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}),
-                                                   rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.3)) !important;
+                background: linear-gradient(90deg, rgb{{ images.0.colours.0 }}, rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.3)) !important;
             }
             @media (max-width: 800px) {
                 .banner-filter {
-                    background: linear-gradient(180deg, rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 1),
-                                                        rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.5)) !important;
+                    background: linear-gradient(180deg, rgba({{ images.0.colours.1.0 }}, {{ images.0.colours.1.1 }}, {{ images.0.colours.1.2 }}, 0.4), rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 0.3)) !important;
                 }
             }
-
-            .navigation {
-                background-color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important;
-            }
-            .navigation-item > i {
-                color: {{ text_colour }} !important;
-            }
-            .navigation-item.selected::before {
-                background-color: {{ text_colour }} !important;
-            }
         {% endif %}
     </style>
 {% endblock %}
-{% block content %}
+
+{% block header %}
     {% if images %}
         <div class="banner">
-            <img src="{{ url_for('media_api.media', path='uploads/' + images.0.filename ) }}?r=prev" onload="imgFade(this)" style="opacity:0;" alt="{% if images.0.alt %}{{ images.0.alt }}{% else %}Group Banner{% endif %}"/>
+            <picture>
+                <source srcset="{{ url_for('api.media', path='uploads/' + images.0.filename) }}?r=prev&e=webp">
+                <source srcset="{{ url_for('api.media', path='uploads/' + images.0.filename) }}?r=prev&e=png">
+                <img
+                    src="{{ url_for('api.media', path='uploads/' + images.0.filename) }}?r=prev"
+                    alt="{% if images.0.alt %}{{ images.0.alt }}{% else %}Group Banner{% endif %}"
+                    onload="imgFade(this)" style="opacity:0;"
+                />
+            </picture>
             <span class="banner-filter"></span>
             <div class="banner-content">
                 <p class="banner-info"><a href="{{ url_for('profile.profile', id=group.author.id) }}" class="link">By {{ group.author.username }}</a></p>
@@ -229,12 +78,12 @@
                 <p class="banner-subtitle">{{ images|length }} Images · {{ group.description }}</p>
                 <div class="pill-row">
                     <div>
-                        <button class="pill-item" onclick="groupShare()"><i class="ph ph-export"></i></button>
+                        <button class="pill-item" onclick="copyToClipboard(window.location.href)"><i class="ph ph-export"></i></button>
                     </div>
                     {% if current_user.id == group.author.id %}
                         <div>
-                            <button class="pill-item pill__critical" onclick="groupDelete()"><i class="ph ph-trash"></i></button>
-                            <button class="pill-item pill__critical" onclick="groupEdit()"><i class="ph ph-pencil-simple"></i></button>
+                            <button class="pill-item pill__critical" onclick="groupDeletePopup()"><i class="ph ph-trash"></i></button>
+                            <button class="pill-item pill__critical" onclick="groupEditPopup()"><i class="ph ph-pencil-simple"></i></button>
                         </div>
                     {% endif %}
                 </div>
@@ -247,29 +96,24 @@
                 <p class="banner-info">By {{ group.author.username }}</p>
                 <div class="pill-row">
                     <div>
-                        <button class="pill-item" onclick="groupShare()"><i class="ph ph-export"></i></button>
+                        <button class="pill-item" onclick="copyToClipboard(window.location.href)"><i class="ph ph-export"></i></button>
                     </div>
                     {% if current_user.id == group.author.id %}
                         <div>
-                            <button class="pill-item pill__critical" onclick="groupDelete()"><i class="ph ph-trash"></i></button>
-                            <button class="pill-item pill__critical" onclick="groupEdit()"><i class="ph ph-pencil-simple"></i></button>
+                            <button class="pill-item pill__critical" onclick="groupDeletePopup()"><i class="ph ph-trash"></i></button>
+                            <button class="pill-item pill__critical" onclick="groupEditPopup()"><i class="ph ph-pencil-simple"></i></button>
                         </div>
                     {% endif %}
                 </div>
             </div>
         </div>
     {% endif %}
+{% endblock %}
 
+{% block content %}
     {% if images %}
         <div class="gallery-grid">
-            {% for image in images %}
-                <a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('group.group_post', group_id=group.id, image_id=image.id) }}" style="background-color: rgb({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})">
-                    <div class="image-filter">
-                        <p class="image-title"><span class="time">{{ image.created_at }}</span></p>
-                    </div>
-                    <img alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}" data-src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
-                </a>
-            {% endfor %}
+            {% for image in images %}{{ gallery_item(image) }}{% endfor %}
         </div>
     {% else %}
         <div class="big-text">
diff --git a/onlylegs/templates/image.html b/onlylegs/templates/image.html
index 5cf33d9..2a76cf0 100644
--- a/onlylegs/templates/image.html
+++ b/onlylegs/templates/image.html
@@ -1,215 +1,157 @@
-{% extends 'layout.html' %}
+{% extends 'base.html' %}
 {% block page_index %}
 {% if return_page %}?page={{ return_page }}{% endif %}{% endblock %}
+
 {% block head %}
-    <meta property="og:image" content="{{ url_for('media_api.media', path='uploads/' + image.filename) }}"/>
-    <meta name="theme-color" content="rgb({{ image.colours.0.0 }}{{ image.colours.0.1 }}{{ image.colours.0.2 }})"/>
+    <meta property="og:image" content="{{ url_for('api.media', path='uploads/' + image.filename) }}"/>
+    <meta name="twitter:image" content="{{ url_for('api.media', path='uploads/' + image.filename) }}">
+    <meta name="theme-color" content="rgb{{ image.colours.0 }}"/>
+    <meta name="twitter:card" content="summary_large_image">
 
     <script type="text/javascript">
-        function imageShare() {
-            try {
-                navigator.clipboard.writeText(window.location.href)
-                addNotification("Copied link!", 4);
-            } catch (err) {
-                addNotification("Failed to copy link! Are you on HTTP?", 2);
-            }
-        }
-        function fullscreen() {
-            let info = document.querySelector('.info-container');
-            let wrapper = document.querySelector('.image-grid');
-
-            if (info.classList.contains('collapsed')) {
-                info.classList.remove('collapsed');
-                wrapper.classList.remove('collapsed');
-            } else {
-                info.classList.add('collapsed');
-                wrapper.classList.add('collapsed');
-            }
-        }
-
-        {% if current_user.id == image.author.id %}
-            function imageDelete() {
-                cancelBtn = document.createElement('button');
-                cancelBtn.classList.add('btn-block');
-                cancelBtn.classList.add('transparent');
-                cancelBtn.innerHTML = 'nuuuuuuuu';
-                cancelBtn.onclick = popupDissmiss;
-
-                deleteBtn = document.createElement('button');
-                deleteBtn.classList.add('btn-block');
-                deleteBtn.classList.add('critical');
-                deleteBtn.innerHTML = 'Dewww eeeet!';
-                deleteBtn.onclick = deleteConfirm;
-                
-                popUpShow('DESTRUCTION!!!!!!',
-                          'Do you want to delete this image along with all of its data??? ' +
-                          'This action is irreversible!',
-                          null,
-                          [cancelBtn, deleteBtn]);
-            }
-            function deleteConfirm() {
-                popupDissmiss();
-
-                fetch('{{ url_for('media_api.delete_image', image_id=image['id']) }}', {
-                    method: 'POST',
-                    headers: {
-                        'Content-Type': 'application/json'
-                    },
-                    body: JSON.stringify({
-                        action: 'delete'
-                    })
-                }).then(function(response) {
-                    if (response.ok) {
-                        window.location.href = '/';
-                    } else {
-                        addNotification(`Image *clings*`, 2);
-                    }
-                });
-            }
-
-            function imageEdit() {
-                addNotification("Not an option, oops!", 3);
-            }
-        {% endif %}
+        const image_data = {
+            'id': {{ image.id }},
+            'description': '{{ image.description }}',
+            'alt': '{{ image.alt }}',
+        };
     </script>
 
     <style>
-        .background span {
+        .background::after {
             background-image: linear-gradient(to top, rgba({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }}, 0.8),
                                                       rgba({{ image.colours.1.0 }}, {{ image.colours.1.1 }}, {{ image.colours.1.2 }}, 0.2));
         }
     </style>
 {% endblock %}
-{% block content %}
-    <div class="background">
-        <img src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=prev" alt="{{ image.alt }}" onload="imgFade(this)" style="opacity:0;"/>
-        <span></span>
-    </div>
-
-    <div class="image-grid">
-        <div class="image-block">
-            <div class="image-container">
-                <img
-                    src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=prev"
-                    alt="{{ image.alt }}"
-                    onload="imgFade(this)"
-                    style="opacity: 0;"
-                    onerror="this.src='{{ url_for('static', filename='error.png')}}'"
-                    {% if "File" in image.exif %}
-                        width="{{ image.exif.File.Width.raw }}"
-                        height="{{ image.exif.File.Height.raw }}"
-                    {% endif %}
-                />
-            </div>
 
+{% block header %}
+    <div class="banner-small">
+        <div class="banner-content">
+            <h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1>
             <div class="pill-row">
                 {% if next_url %}<div><a class="pill-item" href="{{ next_url }}"><i class="ph ph-arrow-left"></i></a></div>{% endif %}
                 <div>
-                    <button class="pill-item" onclick="fullscreen()" id="fullscreenImage"><i class="ph ph-info"></i></button>
-                    <button class="pill-item" onclick="imageShare()"><i class="ph ph-export"></i></button>
-                    <a class="pill-item" href="{{ url_for('media_api.media', path='uploads/' + image.filename) }}" download onclick="addNotification('Download started!', 4)"><i class="ph ph-file-arrow-down"></i></a>
+                    <button class="pill-item" onclick="imageFullscreen()" id="fullscreenImage"><i class="ph ph-info"></i></button>
+                    <button class="pill-item" onclick="copyToClipboard(window.location.href)"><i class="ph ph-export"></i></button>
+                    <a class="pill-item" href="{{ url_for('api.media', path='uploads/' + image.filename) }}" download onclick="addNotification('Download started!', 4)"><i class="ph ph-file-arrow-down"></i></a>
                 </div>
                 {% if current_user.id == image.author.id %}
                     <div>
-                        <button class="pill-item pill__critical" onclick="imageDelete()"><i class="ph ph-trash"></i></button>
-                        <button class="pill-item pill__critical" onclick="imageEdit()"><i class="ph ph-pencil-simple"></i></button>
+                        <button class="pill-item pill__critical" onclick="imageDeletePopup()"><i class="ph ph-trash"></i></button>
+                        <button class="pill-item pill__critical" onclick="imageEditPopup()"><i class="ph ph-pencil-simple"></i></button>
                     </div>
                 {% endif %}
                 {% if prev_url %}<div><a class="pill-item" href="{{ prev_url }}"><i class="ph ph-arrow-right"></i></a></div>{% endif %}
             </div>
         </div>
-    
-        <div class="info-container">
-            <div class="info-tab">
-                <div class="info-header">
-                    <i class="ph ph-info"></i>
-                    <h2>Info</h2>
-                    <button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
-                </div>
-                <div class="info-table">
-                    <table>
-                        <tr>
-                            <td>Author</td>
-                            <td><a href="{{ url_for('profile.profile', id=image.author.id) }}" class="link">{{ image.author.username }}</a></td>
-                        </tr>
-                        <tr>
-                            <td>Upload date</td>
-                            <td><span class="time">{{ image.created_at }}</span></td>
-                        </tr>
-                        {% if image.description %}
-                            <tr>
-                                <td>Description</td>
-                                <td>{{ image.description }}</td>
-                            </tr>
-                        {% endif %}
-                    </table>
-                    <div class="img-colours">
-                        {% for col in image.colours %}
-                            <span style="background-color: rgb({{col.0}}, {{col.1}}, {{col.2}})"></span>
-                        {% endfor %}
-                    </div>
-                    {% if image.groups %}
-                        <div class="img-groups">
-                            {% for group in image.groups %}
-                                <a href="{{ url_for('group.group', group_id=group.id) }}" class="tag-icon">
-                                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M223.68,66.15,135.68,18a15.88,15.88,0,0,0-15.36,0l-88,48.17a16,16,0,0,0-8.32,14v95.64a16,16,0,0,0,8.32,14l88,48.17a15.88,15.88,0,0,0,15.36,0l88-48.17a16,16,0,0,0,8.32-14V80.18A16,16,0,0,0,223.68,66.15ZM128,32l80.34,44-29.77,16.3-80.35-44ZM128,120,47.66,76l33.9-18.56,80.34,44ZM40,90l80,43.78v85.79L40,175.82Zm176,85.78h0l-80,43.79V133.82l32-17.51V152a8,8,0,0,0,16,0V107.55L216,90v85.77Z"></path></svg>
-                                    {{ group['name'] }}
-                                </a>
-                            {% endfor %}
-                        </div>
-                    {% endif %}
-                </div>
-            </div>       
-            {% for tag in image.exif %}
-                <div class="info-tab">
-                    <div class="info-header">
-                        {% if tag == 'Photographer' %}
-                            <i class="ph ph-person"></i><h2>Photographer</h2>
-                        {% elif tag == 'Camera' %}
-                            <i class="ph ph-camera"></i><h2>Camera</h2>
-                        {% elif tag == 'Software' %}
-                            <i class="ph ph-desktop-tower"></i><h2>Software</h2>
-                        {% elif tag == 'File' %}
-                            <i class="ph ph-file-image"></i><h2>File</h2>
-                        {% else %}
-                            <i class="ph ph-file-image"></i><h2>{{ tag }}</h2>
-                        {% endif %}
-                        <button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
-                    </div>
-                    <div class="info-table">
-                        <table>
-                            {% for subtag in image.exif[tag] %}
-                                <tr>
-                                    <td>{{ subtag }}</td>
-                                    {% if image.exif[tag][subtag]['formatted'] %}
-                                        {% if image.exif[tag][subtag]['type'] == 'date' %}
-                                            <td><span class="time">{{ image.exif[tag][subtag]['formatted'] }}</span></td>
-                                        {% else %}
-                                            <td>{{ image.exif[tag][subtag]['formatted'] }}</td>
-                                        {% endif %}
-                                    {% elif image.exif[tag][subtag]['raw'] %}
-                                        <td>{{ image.exif[tag][subtag]['raw'] }}</td>
-                                    {% else %}
-                                        <td class="empty-table">Oops, an error</td>
-                                    {% endif %}
-                                </tr>
-                            {% endfor %}
-                        </table>
-                    </div>
-                </div>
-            {% endfor %}
-        </div>
     </div>
 {% endblock %}
 
-{% block script %}
-    <script type="text/javascript">
-        let infoTab = document.querySelectorAll('.info-tab');
+{% block content %}
+    <div class="background">
+        <picture>
+            <source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev&e=webp">
+            <source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev&e=png">
+            <img src="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev" alt="{{ image.alt }}" onload="imgFade(this)" style="opacity:0;"/>
+        </picture>
+    </div>
 
-        for (let i = 0; i < infoTab.length; i++) {
-            infoTab[i].querySelector('.collapse-indicator').addEventListener('click', function() {
-                infoTab[i].classList.toggle('collapsed');
-            });
-        }
-    </script>
-{% endblock %}
\ No newline at end of file
+    <div class="image-container {% if close_tab %}collapsed{% endif %}">
+        <picture>
+            <source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev&e=webp">
+            <source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev&e=png">
+            <img
+                src="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=prev"
+                alt="{{ image.alt }}"
+                onload="imgFade(this)"
+                style="opacity:0;"
+                {% if "File" in image.exif %}
+                    width="{{ image.exif.File.Width.raw }}"
+                    height="{{ image.exif.File.Height.raw }}"
+                {% endif %}
+            />
+        </picture>
+    </div>
+
+    <div class="info-container {% if close_tab %}collapsed{% endif %}">
+        <details open>
+            <summary>
+                <i class="ph ph-info"></i><h2>Info</h2><span style="width: 100%"></span>
+                <i class="ph ph-caret-down collapse-indicator"></i>
+            </summary>
+
+            <table>
+                <tr>
+                    <td>Author</td>
+                    <td>
+                        {% if image.author.picture %}
+                            <img src="{{ url_for('api.media', path='pfp/' + image.author.picture) }}" alt="Profile Picture" class="pfp" onload="imgFade(this)" style="opacity: 0;"/>
+                        {% endif %}
+                        <a href="{{ url_for('profile.profile', id=image.author.id) }}" class="link">{{ image.author.username }}</a>
+                    </td>
+                </tr>
+                <tr>
+                    <td>Upload date</td>
+                    <td><span class="time">{{ image.created_at }}</span></td>
+                </tr>
+                {% if image.description %}
+                    <tr>
+                        <td>Description</td>
+                        <td>{{ image.description }}</td>
+                    </tr>
+                {% endif %}
+            </table>
+            <div class="img-colours">
+                {% for col in image.colours %}
+                    <button style="background-color: rgb{{ col }}" onclick="copyToClipboard('rgb{{ col }}')">
+                        <i class="ph-fill ph-paint-bucket" style="color:{{ col|colour_contrast }};"></i>
+                    </button>
+                {% endfor %}
+            </div>
+            {% if image.groups %}
+                <div class="img-groups">
+                    {% for group in image.groups %}
+                        <a href="{{ url_for('group.group', group_id=group.id) }}" class="tag-icon"><i class="ph ph-package"></i>{{ group['name'] }}</a>
+                    {% endfor %}
+                </div>
+            {% endif %}
+        </details>
+        {% for tag in image.exif %}
+            <details open>
+                <summary>
+                    {% if tag == 'Photographer' %}
+                        <i class="ph ph-person"></i><h2>Photographer</h2>
+                    {% elif tag == 'Camera' %}
+                        <i class="ph ph-camera"></i><h2>Camera</h2>
+                    {% elif tag == 'Software' %}
+                        <i class="ph ph-desktop-tower"></i><h2>Software</h2>
+                    {% elif tag == 'File' %}
+                        <i class="ph ph-file-image"></i><h2>File</h2>
+                    {% else %}
+                        <i class="ph ph-file-image"></i><h2>{{ tag }}</h2>
+                    {% endif %}
+                    <span style="width: 100%"></span>
+                    <i class="ph ph-caret-down collapse-indicator"></i>
+                </summary>
+                <table>
+                    {% for subtag in image.exif[tag] %}
+                        <tr>
+                            <td>{{ subtag }}</td>
+                            {% if image.exif[tag][subtag]['formatted'] %}
+                                {% if image.exif[tag][subtag]['type'] == 'date' %}
+                                    <td><span class="time">{{ image.exif[tag][subtag]['formatted'] }}</span></td>
+                                {% else %}
+                                    <td>{{ image.exif[tag][subtag]['formatted'] }}</td>
+                                {% endif %}
+                            {% elif image.exif[tag][subtag]['raw'] %}
+                                <td>{{ image.exif[tag][subtag]['raw'] }}</td>
+                            {% else %}
+                                <td class="empty-table">Oops, an error</td>
+                            {% endif %}
+                        </tr>
+                    {% endfor %}
+                </table>
+            </details>
+        {% endfor %}
+    </div>
+{% endblock %}
diff --git a/onlylegs/templates/index.html b/onlylegs/templates/index.html
index 340313f..eead3a8 100644
--- a/onlylegs/templates/index.html
+++ b/onlylegs/templates/index.html
@@ -1,11 +1,17 @@
-{% extends 'layout.html' %}
-{% block nav_home %}selected{% endblock %}
-{% block content %}
+{% extends 'base.html' %}
+{% from 'macros/image.html' import gallery_item %}
+{% block head %}
+    <meta property="og:image" content="{{ url_for('static', filename='icon.png') }}"/>
+    <meta name="twitter:image" content="{{ url_for('static', filename='icon.png') }}"/>
+    <meta name="twitter:card" content="summary"/>
+{% endblock %}
+
+{% block header %}
     <div class="banner-small">
         <div class="banner-content">
             <h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1>
-            {% if total_images == 0 %}
-                <p class="banner-info">0 images D:</p>
+            {% if not total_images %}
+                <p class="banner-info">0 images!</p>
             {% elif total_images == 69 %}
                 <p class="banner-info">{{ total_images }} images, nice</p>
             {% else %}
@@ -15,29 +21,33 @@
             {% if pages > 1 %}
                 <div class="pill-row">
                     <div>
-                        {% if pages > 4 %}<a class="pill-item" href="{{ url_for('gallery.index') }}"><i class="ph ph-arrow-line-left"></i></a>{% endif %}
-                        <a class="pill-item" href="{% if (page - 1) > 1 %} {{ url_for('gallery.index', page=page-1) }} {% else %} {{ url_for('gallery.index') }} {% endif %}"><i class="ph ph-arrow-left"></i></a>
+                        {% if pages > 4 %}
+                            <a class="pill-item" href="{{ url_for('gallery.index') }}"><i class="ph ph-caret-double-left"></i></a>
+                        {% else %}
+                            <button class="pill-item disabled"><i class="ph ph-caret-double-left"></i></button>
+                        {% endif %}
+                        <a class="pill-item" href="{% if (page - 1) > 1 %} {{ url_for('gallery.index', page=page - 1) }} {% else %} {{ url_for('gallery.index') }} {% endif %}"><i class="ph ph-caret-left"></i></a>
                     </div>
                     <span class="pill-text">{{ page }} / {{ pages }}</span>
                     <div>
-                        <a class="pill-item" href="{% if (page + 1) < pages %} {{ url_for('gallery.index', page=page+1) }} {% else %} {{ url_for('gallery.index', page=pages) }} {% endif %}"><i class="ph ph-arrow-right"></i></a>
-                        {% if pages > 4 %}<a class="pill-item" href="{{ url_for('gallery.index', page=pages) }}"><i class="ph ph-arrow-line-right"></i></a>{% endif %}
+                        <a class="pill-item" href="{% if (page + 1) < pages %} {{ url_for('gallery.index', page=page + 1) }} {% else %} {{ url_for('gallery.index', page=pages) }} {% endif %}"><i class="ph ph-caret-right"></i></a>
+                        {% if pages > 4 %}
+                            <a class="pill-item" href="{{ url_for('gallery.index', page=pages) }}"><i class="ph ph-caret-double-right"></i></a>
+                        {% else %}
+                            <button class="pill-item disabled"><i class="ph ph-caret-double-right"></i></button>
+                        {% endif %}
                     </div>
                 </div>
             {% endif %}
         </div>
     </div>
+{% endblock %}
 
+{% block nav_home %}selected{% endblock %}
+{% block content %}
     {% if images %}
         <div class="gallery-grid">
-            {% for image in images %}
-                <a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('image.image', image_id=image.id) }}" style="background-color: rgb({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})">
-                    <div class="image-filter">
-                        <p class="image-title"><span class="time">{{ image.created_at }}</span></p>
-                    </div>
-                    <img fetchpriority="low" alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}" data-src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
-                </a>
-            {% endfor %}
+            {% for image in images %}{{ gallery_item(image) }}{% endfor %}
         </div>
     {% else %}
         <div class="big-text">
diff --git a/onlylegs/templates/layout.html b/onlylegs/templates/layout.html
deleted file mode 100644
index e01adb2..0000000
--- a/onlylegs/templates/layout.html
+++ /dev/null
@@ -1,157 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <title>{{ config.WEBSITE_CONF.name }}</title>
-
-    <meta charset="UTF-8">
-
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta name="description" content="{{ config.WEBSITE_CONF.motto }}"/>
-    <meta name="author" content="{{ config.WEBSITE_CONF.author }}"/>
-
-    <meta property="og:title" content="{{ config.WEBSITE_CONF.name }}"/>
-    <meta property="og:description" content="{{ config.WEBSITE_CONF.motto }}"/>
-    <meta property="og:type" content="website"/>
-
-    <meta name="twitter:title" content="{{ config.WEBSITE_CONF.name }}"/>
-    <meta name="twitter:description" content="{{ config.WEBSITE_CONF.motto }}"/>
-    <meta name="twitter:card" content="summary_large_image">
-
-    <link rel="manifest" href="static/manifest.json"/>
-
-    <!-- phosphor icons!!! -->
-    <script src="https://unpkg.com/@phosphor-icons/web"></script>
-    
-    <link
-        href="{{url_for('static', filename='logo-black.svg')}}"
-        rel="icon"
-        type="image/svg+xml"
-        media="(prefers-color-scheme: light)"/>
-    <link
-        href="{{url_for('static', filename='logo-white.svg')}}"
-        rel="icon"
-        type="image/svg+xml"
-        media="(prefers-color-scheme: dark)"/>
-
-    <link
-        rel="prefetch"
-        href="{{url_for('static', filename='fonts/font.css')}}"
-        type="stylesheet"/>
-
-    {% assets "scripts" %}
-        <script type="text/javascript" src="{{ ASSET_URL }}"></script>
-    {% endassets %}
-
-    {% assets "styles" %}
-        <link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css" defer>
-    {% endassets %}
-    
-    {% block head %}{% endblock %}
-</head>
-<body>
-    <div class="notifications"></div>
-
-    <button class="top-of-page" aria-label="Jump to top of page"><i class="ph ph-arrow-up"></i></button>
-    {% if request.path == "/" %}<button class="info-button" aria-label="Show info on gallery"><i class="ph ph-question"></i></button>{% endif %}
-
-    <div class="pop-up">
-        <span class="pop-up__click-off" onclick="popupDissmiss()"></span>
-        <div class="pop-up-wrapper">
-            <div class="pop-up-header"></div>
-            <div class="pop-up-controlls"></div>
-        </div>
-    </div>
-
-    <div class="wrapper">
-
-        <div class="navigation">
-            <!--<img src="{{url_for('static', filename='icon.png')}}" alt="Logo" class="logo" onload="this.style.opacity=1;" style="opacity:0">-->
-
-            <a href="{{ url_for('gallery.index') }}{% block page_index %}{% endblock %}" class="navigation-item {% block nav_home %}{% endblock %}">
-                <i class="ph-fill ph-images-square"></i>
-                <span class="tool-tip">Home<i class="ph-fill ph-caret-left"></i></span>
-            </a>
-
-            <a href="{{ url_for('group.groups') }}" class="navigation-item {% block nav_groups %}{% endblock %}">
-                <i class="ph-fill ph-package"></i>
-                <span class="tool-tip">Groups<i class="ph-fill ph-caret-left"></i></span>
-            </a>
-
-            {% if current_user.is_authenticated %}
-                <button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()">
-                    <i class="ph-fill ph-upload"></i>
-                    <span class="tool-tip">Upload<i class="ph-fill ph-caret-left"></i></span>
-                </button>
-            {% endif %}
-
-            <span class="navigation-spacer"></span>
-
-            {% if current_user.is_authenticated %}
-                <a href="{{ url_for('profile.profile') }}" class="navigation-item {% block nav_profile %}{% endblock %}">
-                    {% if current_user.picture %}
-                        <span class="nav-pfp">
-                            <img
-                                src="{{ url_for('media_api.media', path='pfp/' + current_user.picture) }}?r=icon"
-                                alt="Profile picture"
-                                onload="imgFade(this)"
-                                style="opacity:0;"
-                            />
-                        </span>
-                    {% else %}
-                        <i class="ph-fill ph-folder-simple-user"></i>
-                    {% endif %}
-                    <span class="tool-tip">Profile<i class="ph-fill ph-caret-left"></i></span>
-                </a>
-            
-                <a href="{{ url_for('settings.general') }}" class="navigation-item {% block nav_settings %}{% endblock %}">
-                    <i class="ph-fill ph-gear-fine"></i>
-                    <span class="tool-tip">Settings<i class="ph-fill ph-caret-left"></i></span>
-                </a>
-            {% else %}
-                <button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()">
-                    <i class="ph-fill ph-sign-in"></i>
-                    <span class="tool-tip">Login<i class="ph-fill ph-caret-left"></i></span>
-                </button>
-            {% endif %}
-        </div>
-
-        {% if current_user.is_authenticated %}
-            <div class="upload-panel">
-                <span class="click-off" onclick="closeUploadTab()"></span>
-                <div class="container">
-                    <span id="dragIndicator"></span>
-                    <h3>Upload stuffs</h3>
-                    <p>May the world see your stuff 👀</p>
-                    <form id="uploadForm">
-                        <button class="fileDrop-block" type="button">
-                            <i class="ph ph-upload"></i>
-                            <span class="status">Choose or Drop file</span>
-                            <input type="file" id="file" tab-index="-1"/>
-                        </button>
-
-                        <input class="input-block" type="text" placeholder="alt" id="alt"/>
-                        <input class="input-block" type="text" placeholder="description" id="description"/>
-                        <input class="input-block" type="text" placeholder="tags" id="tags"/>
-                        <button class="btn-block primary" type="submit">Upload</button>
-                    </form>
-                    <div class="upload-jobs"></div>
-                </div>
-            </div>
-        {% endif %}
-
-        <div class="content">
-            {% block content %}
-            {% endblock %}
-        </div>
-    </div>
-
-    <script type="text/javascript">
-        // Show notifications on page load
-        {% for message in get_flashed_messages() %}
-            addNotification('{{ message[0] }}', {{ message[1] }});
-        {% endfor %}
-    </script>
-
-    {% block script %}{% endblock %}
-</body>
-</html>
\ No newline at end of file
diff --git a/onlylegs/templates/list.html b/onlylegs/templates/list.html
index af2edaa..21a7c9b 100644
--- a/onlylegs/templates/list.html
+++ b/onlylegs/templates/list.html
@@ -1,100 +1,11 @@
-{% extends 'layout.html' %}
+{% extends 'base.html' %}
 {% block nav_groups %}selected{% endblock %}
+
 {% block head %}
-    {% if images %}
-        <meta name="theme-color" content="rgb({{ images.0.colours.0.0 }}{{ images.0.colours.0.1 }}{{ images.0.colours.0.2 }})"/>
-    {% endif %}
-
-    {% if current_user.is_authenticated %}
-        <script type="text/javascript">
-            function showCreate() {
-                // Create elements
-                cancelBtn = document.createElement('button');
-                cancelBtn.classList.add('btn-block');
-                cancelBtn.classList.add('transparent');
-                cancelBtn.innerHTML = 'nuuuuuuuu';
-                cancelBtn.onclick = popupDissmiss;
-
-                submitBtn = document.createElement('button');
-                submitBtn.classList.add('btn-block');
-                submitBtn.classList.add('primary');
-                submitBtn.innerHTML = 'Submit!!';
-                submitBtn.type = 'submit';
-                submitBtn.setAttribute('form', 'createForm');
-
-                // Create form
-                createForm = document.createElement('form');
-                createForm.id = 'createForm';
-                createForm.setAttribute('onsubmit', 'return create(event);');
-
-                titleInput = document.createElement('input');
-                titleInput.classList.add('input-block');
-                titleInput.type = 'text';
-                titleInput.placeholder = 'Group namey';
-                titleInput.id = 'name';
-
-                descriptionInput = document.createElement('input');
-                descriptionInput.classList.add('input-block');
-                descriptionInput.type = 'text';
-                descriptionInput.placeholder = 'What it about????';
-                descriptionInput.id = 'description';
-
-                createForm.appendChild(titleInput);
-                createForm.appendChild(descriptionInput);
-
-                popUpShow(
-                    'New stuff!',
-                    'Image groups are a simple way to "group" images together, are you ready?',
-                    createForm,
-                    [cancelBtn, submitBtn]
-                );
-            }
-
-            function create(event) {
-                // AJAX takes control of subby form :3
-                event.preventDefault();
-            
-                let formName = document.querySelector("#name").value;
-                let formDescription = document.querySelector("#description").value;
-            
-                if (!formName) {
-                    addNotification("Group name must be set!", 3);
-                    return;
-                }
-            
-                // Make form
-                const formData = new FormData();
-                formData.append("name", formName);
-                formData.append("description", formDescription);
-            
-                fetch('{{ url_for('group_api.create_group') }}', {
-                    method: 'POST',
-                    body: formData
-                }).then(response => {
-                    if (response.status === 200) {
-                        addNotification('Group created!', 1);
-                        popupDissmiss();
-                    } else {
-                        switch (response.status) {
-                            case 500:
-                                addNotification('Server exploded, F\'s in chat', 2);
-                                break;
-                            case 403:
-                                addNotification('None but devils play past here... Bad information', 2);
-                                break;
-                            default:
-                                addNotification('Error logging in, blame someone', 2);
-                                break;
-                        }
-                    }
-                }).catch(error => {
-                    addNotification('Error making group! :c', 2);
-                });
-            }
-        </script>
-    {% endif %}
+    {% if images %}<meta name="theme-color" content="rgb{{ images.0.colours.0 }}"/>{% endif %}
 {% endblock %}
-{% block content %}
+
+{% block header %}
     <div class="banner-small">
         <div class="banner-content">
             <h1 class="banner-header">{{ config.WEBSITE_CONF.name }}</h1>
@@ -108,17 +19,24 @@
             {% if current_user.is_authenticated %}
                 <div class="pill-row">
                     <div>
-                        <button class="pill-item" onclick="showCreate()"><i class="ph ph-plus"></i></button>
+                        <button class="pill-item" onclick="groupCreatePopup()"><i class="ph ph-plus"></i></button>
                     </div>
                 </div>
             {% endif %}
         </div>
     </div>
+{% endblock %}
 
+{% block content %}
     {% if groups %}
         <div class="gallery-grid">
             {% for group in groups %}
-                <a id="group-{{ group.id }}" class="group-item" href="{{ url_for('group.group', group_id=group.id) }}" {% if group.images|length > 0 %} style="background-color: rgba({{ group.images.0.colours.0.0 }}, {{ group.images.0.colours.0.1 }}, {{ group.images.0.colours.0.2 }}, 0.4);" {% endif %}>
+                <a
+                        class="group-item square"
+                        id="group-{{ group.id }}"
+                        href="{{ url_for('group.group', group_id=group.id) }}"
+                        {% if group.images|length > 0 %} style="background-color: rgba{{ group.images.0.colours.0 }};"{% endif %}
+                >
                     <div class="image-filter">
                         <p class="image-subtitle">By {{ group.author.username }}</p>
                         <p class="image-title">{{ group.name }}</p>
@@ -126,7 +44,18 @@
                     <div class="images size-{{ group.images|length }}">
                         {% if group.images|length > 0 %}
                             {% for image in group.images %}
-                                <img data-src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load" class="data-{{ loop.index }}" {% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}/>
+                                <picture>
+                                    <source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb&e=webp">
+                                    <source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb&e=png">
+                                    <img
+                                        src="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb"
+                                        alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}"
+                                        class="data-{{ loop.index }}"
+                                        onload="imgFade(this)"
+                                        style="opacity:0;"
+                                        fetchpriority="low"
+                                    />
+                                </picture>
                             {% endfor %}
                         {% else %}
                             <img src="{{ url_for('static', filename='error.png') }}" class="loaded" alt="Error thumbnail"/>
diff --git a/onlylegs/templates/macros/header.html b/onlylegs/templates/macros/header.html
new file mode 100644
index 0000000..4433fd0
--- /dev/null
+++ b/onlylegs/templates/macros/header.html
@@ -0,0 +1,9 @@
+{% macro header_small(title, subtitle, buttons) %}
+    <div class="banner-small">
+        <div class="banner-content">
+            <h1 class="banner-header">{{ title }}</h1>
+            <p class="banner-info">{{ subtitle }}</p>
+            <div class="pill-row">{{ buttons }}</div>
+        </div>
+    </div>
+{% endmacro %}
diff --git a/onlylegs/templates/macros/image.html b/onlylegs/templates/macros/image.html
new file mode 100644
index 0000000..ed66b28
--- /dev/null
+++ b/onlylegs/templates/macros/image.html
@@ -0,0 +1,23 @@
+{% macro gallery_item(image) %}
+    <a
+        id="image-{{ image.id }}"
+        class="gallery-item square"
+        href="{{ url_for('image.image', image_id=image.id) }}"
+        style="background-color: rgb{{ image.colours.0 }}"
+        draggable="false">
+        <div class="image-filter">
+            <p class="image-subtitle">By {{ image.username }}</p>
+            <p class="image-title"><span class="time">{{ image.created_at }}</span></p>
+        </div>
+        <picture>
+            <source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb&e=webp">
+            <source srcset="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb&e=png">
+            <img
+                src="{{ url_for('api.media', path='uploads/' + image.filename) }}?r=thumb"
+                alt="{% if image.alt %}{{ image.alt }}{% else %}Image Thumbnail{% endif %}"
+                onload="imgFade(this)"
+                style="opacity:0;"
+            />
+        </picture>
+    </a>
+{% endmacro %}
diff --git a/onlylegs/templates/profile.html b/onlylegs/templates/profile.html
index 7b4ae81..7880339 100644
--- a/onlylegs/templates/profile.html
+++ b/onlylegs/templates/profile.html
@@ -1,15 +1,18 @@
-{% extends 'layout.html' %}
+{% extends 'base.html' %}
+{% from 'macros/image.html' import gallery_item %}
+{% block nav_profile %}{% if user.id == current_user.id %}selected{% endif %}{% endblock %}
+
 {% block head %}
     {% if user.picture %}
-        <meta property="og:image" content="{{ url_for('media_api.media', path='pfp/' + user.picture) }}"/>
-    {% endif %}
-    {% if user.colour %}
-        <meta name="theme-color" content="rgb({{ user.colour.0 }}, {{ user.colour.1 }}, {{ user.colour.2 }})"/>
+        <meta property="og:image" content="{{ url_for('api.media', path='pfp/' + user.picture) }}"/>
+        <meta name="twitter:image" content="{{ url_for('api.media', path='pfp/' + user.picture) }}">
     {% endif %}
+    {% if user.colour %}<meta name="theme-color" content="rgb{{ user.colour }}"/>{% endif %}
+    <meta name="twitter:card" content="summary">
 
     <script type="text/javascript">
         function moreInfo() {
-            popUpShow('{{ user.username }}',
+            popupShow('{{ user.username }}',
                         '<p>Joined: {{ user.joined_at }}</p><br>' +
                         '<p>Images: {{ images|length }}</p><br>' +
                         '<p>Groups: {{ groups|length }}</p>');
@@ -18,12 +21,12 @@
 
     <style>
         .banner-picture {
-            background-color: rgb({{ user.colour.0 }}, {{ user.colour.1 }}, {{ user.colour.2 }}) !important;
+            background-color: rgb{{ user.colour }} !important;
         }
     </style>
 {% endblock %}
-{% block nav_profile %}{% if user.id == current_user.id %}selected{% endif %}{% endblock %}
-{% block content %}
+
+{% block header %}
     <div class="banner">
         {% if user.banner %}
             <img src="{{ url_for('static', filename='icon.png') }}" alt="Profile Banner" onload="imgFade(this)" style="opacity:0;"/>
@@ -33,13 +36,16 @@
         <span class="banner-filter"></span>
         <div class="banner-content">
             {% if user.picture %}
-                <img
-                    class="banner-picture"
-                    src="{{ url_for('media_api.media', path='pfp/' + user.picture) }}?r=pfp"
-                    alt="Profile picture"
-                    onload="imgFade(this)"
-                    style="opacity:0;"
-                />
+                <picture class="banner-picture">
+                    <source srcset="{{ url_for('api.media', path='pfp/' + user.picture) }}?r=pfp&e=webp">
+                    <source srcset="{{ url_for('api.media', path='pfp/' + user.picture) }}?r=pfp&e=png">
+                    <img
+                        src="{{ url_for('api.media', path='pfp/' + user.picture) }}?r=pfp"
+                        alt="Profile picture"
+                        onload="imgFade(this)"
+                        style="opacity:0;"
+                    />
+                </picture>
             {% else %}
                 <img
                     class="banner-picture"
@@ -53,7 +59,7 @@
             <p class="banner-subtitle">{{ images|length }} Images · {{ groups|length }} Groups</p>
             <div class="pill-row">
                 <div>
-                    <button class="pill-item" onclick="profileShare()"><i class="ph ph-export"></i></button>
+                    <button class="pill-item" onclick="copyToClipboard(window.location.href)"><i class="ph ph-export"></i></button>
                     <button class="pill-item" onclick="moreInfo()"><i class="ph ph-info"></i></button>
                 </div>
                 {% if user.id == current_user.id %}
@@ -64,18 +70,13 @@
             </div>
         </div>
     </div>
+{% endblock %}
 
+{% block content %}
     {% if images %}
         <h1 class="gallery-header">Images</h1>
         <div class="gallery-grid">
-            {% for image in images %}
-                <a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('image.image', image_id=image.id) }}" style="background-color: rgb({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})">
-                    <div class="image-filter">
-                        <p class="image-title"><span class="time">{{ image.created_at }}</span></p>
-                    </div>
-                    <img fetchpriority="low" alt="{{ image.alt }}" data-src="{{ url_for('media_api.media', path='uploads/' + image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
-                </a>
-            {% endfor %}
+            {% for image in images %}{{ gallery_item(image) }}{% endfor %}
         </div>
     {% else %}
         <div class="big-text">
@@ -84,4 +85,3 @@
         </div>
     {% endif %}
 {% endblock %}
-
diff --git a/onlylegs/templates/settings.html b/onlylegs/templates/settings.html
index b25e209..10840cb 100644
--- a/onlylegs/templates/settings.html
+++ b/onlylegs/templates/settings.html
@@ -1,7 +1,7 @@
-{% extends 'layout.html' %}
-
+{% extends 'base.html' %}
 {% block nav_settings %}selected{% endblock %}
-{% block content %}
+
+{% block header %}
     <div class="banner-small">
         <div class="banner-content">
             <h1 class="banner-header">Settings</h1>
@@ -13,28 +13,53 @@
             </div>
         </div>
     </div>
+{% endblock %}
 
-    <div class="settings-content" id="profileSettings">
-        <h2>Profile Settings</h2>
-        <form method="POST" action="{{ url_for('account_api.account_picture', user_id=current_user.id) }}" enctype="multipart/form-data">
-            <h3>Profile Picture</h3>
-            <input type="file" name="file" tab-index="-1"/>
-            <input type="submit" value="Upload" class="btn-block">
-        </form>
-    
-        <form method="POST" action="{{ url_for('account_api.account_username', user_id=current_user.id) }}" enctype="multipart/form-data">
-            <h3>Username</h3>
-            <input type="text" name="name" class="input-block" value="{{ current_user.username }}" />
-            <input type="submit" value="Upload" class="btn-block"/>
-        </form>
+{% block content %}
+    <div class="info-tab" id="profileSettings" style="margin: 0.5rem 0.5rem 0 0.5rem">
+        <div class="info-header">
+            <i class="ph ph-info"></i>
+            <h2>Profile Settings</h2>
+            <button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
+        </div>
+        <div class="info-table">
+            <form method="POST" action="{{ url_for('api.account_picture', user_id=current_user.id) }}" enctype="multipart/form-data">
+                <h3>Profile Picture</h3>
+                <input type="file" name="file" tab-index="-1"/>
+                <input type="submit" value="Upload" class="btn-block">
+            </form>
+            <form method="POST" action="{{ url_for('api.account_username', user_id=current_user.id) }}" enctype="multipart/form-data">
+                <h3>Username</h3>
+                <input type="text" name="name" class="input-block" value="{{ current_user.username }}" />
+                <input type="submit" value="Upload" class="btn-block"/>
+            </form>
+        </div>
     </div>
 
-    <div class="settings-content" id="profileSettings">
-        <h2>Account Settings</h2>    
-        <form method="POST" action="" enctype="multipart/form-data">
-            <h3>Email</h3>
-            <input type="text" name="email" class="input-block" value="{{ current_user.email }}" />
-            <input type="submit" value="Upload" class="btn-block"/>
-        </form>
+    <div class="info-tab" id="profileSettings" style="margin: 0.5rem 0.5rem 0 0.5rem">
+        <div class="info-header">
+            <i class="ph ph-info"></i>
+            <h2>Account Settings</h2>
+            <button class="collapse-indicator"><i class="ph ph-caret-down"></i></button>
+        </div>
+        <div class="info-table">
+            <form method="POST" action="" enctype="multipart/form-data">
+                <h3>Email</h3>
+                <input type="text" name="email" class="input-block" value="{{ current_user.email }}" />
+                <input type="submit" value="Upload" class="btn-block"/>
+            </form>
+        </div>
     </div>
-{% endblock %}
\ No newline at end of file
+{% endblock %}
+
+{% block script %}
+    <script type="text/javascript">
+        let infoTab = document.querySelectorAll('.info-tab');
+
+        for (let i = 0; i < infoTab.length; i++) {
+            infoTab[i].querySelector('.collapse-indicator').addEventListener('click', function() {
+                infoTab[i].classList.toggle('collapsed');
+            });
+        }
+    </script>
+{% endblock %}
diff --git a/onlylegs/utils/colour.py b/onlylegs/utils/colour.py
new file mode 100644
index 0000000..4c9c8b0
--- /dev/null
+++ b/onlylegs/utils/colour.py
@@ -0,0 +1,69 @@
+"""
+Colour tools used by OnlyLegs
+
+Source 1: https://gist.github.com/mathebox/e0805f72e7db3269ec22
+"""
+
+
+class Colour:
+    def __init__(self, rgb):
+        self.rgb = rgb
+
+    def is_light(self, threshold=0.179):
+        """
+        returns True if background is light, False if dark
+        threshold: the threshold to use for determining lightness, the default is w3 recommended
+        """
+        red, green, blue = self.rgb
+
+        # Calculate contrast
+        colors = [red / 255, green / 255, blue / 255]
+        cont = [
+            col / 12.92 if col <= 0.03928 else ((col + 0.055) / 1.055) ** 2.4
+            for col in colors
+        ]
+        lightness = (0.2126 * cont[0]) + (0.7152 * cont[1]) + (0.0722 * cont[2])
+
+        return True if lightness > threshold else False
+
+    def to_hsv(self):
+        r, g, b = self.rgb
+        high = max(r, g, b)
+        low = min(r, g, b)
+        h, s, v = high, high, high
+
+        d = high - low
+        s = 0 if high == 0 else d / high
+
+        if high == low:
+            h = 0.0
+        else:
+            h = {
+                r: (g - b) / d + (6 if g < b else 0),
+                g: (b - r) / d + 2,
+                b: (r - g) / d + 4,
+            }[high]
+            h /= 6
+
+        return h, s, v
+
+    def to_hsl(self):
+        r, g, b = self.rgb
+        high = max(r, g, b)
+        low = min(r, g, b)
+        h, s, v = ((high + low) / 2,) * 3
+
+        if high == low:
+            h = 0.0
+            s = 0.0
+        else:
+            d = high - low
+            s = d / (2 - high - low) if l > 0.5 else d / (high + low)
+            h = {
+                r: (g - b) / d + (6 if g < b else 0),
+                g: (b - r) / d + 2,
+                b: (r - g) / d + 4,
+            }[high]
+            h /= 6
+
+        return h, s, v
diff --git a/onlylegs/utils/contrast.py b/onlylegs/utils/contrast.py
deleted file mode 100644
index 2872914..0000000
--- a/onlylegs/utils/contrast.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""
-Calculate the contrast between two colors
-"""
-
-
-def contrast(background, light, dark, threshold=0.179):
-    """
-    background: tuple of (r, g, b) values
-    light: color to use if the background is light
-    dark: color to use if the background is dark
-    threshold: the threshold to use for determining lightness, the default is w3 recommended
-    """
-    red = background[0]
-    green = background[1]
-    blue = background[2]
-
-    # Calculate contrast
-    uicolors = [red / 255, green / 255, blue / 255]
-    cont = [
-        col / 12.92 if col <= 0.03928 else ((col + 0.055) / 1.055) ** 2.4
-        for col in uicolors
-    ]
-    lightness = (0.2126 * cont[0]) + (0.7152 * cont[1]) + (0.0722 * cont[2])
-
-    return light if lightness > threshold else dark
diff --git a/onlylegs/utils/generate_image.py b/onlylegs/utils/generate_image.py
index fd86d28..c597d3d 100644
--- a/onlylegs/utils/generate_image.py
+++ b/onlylegs/utils/generate_image.py
@@ -7,7 +7,7 @@ from werkzeug.utils import secure_filename
 from onlylegs.config import MEDIA_FOLDER, CACHE_FOLDER
 
 
-def generate_thumbnail(file_path, resolution, ext=None):
+def generate_thumbnail(file_path, resolution, ext=""):
     """
     Image thumbnail generator
     Uses PIL to generate a thumbnail of the image and saves it to the cache directory
@@ -25,25 +25,25 @@ def generate_thumbnail(file_path, resolution, ext=None):
     if not ext:
         ext = file_ext.strip(".")
 
-    # PIL doesnt like jpg so we convert it to jpeg
-    if ext.lower() == "jpg":
-        ext = "jpeg"
+    ext = "jpeg" if ext.lower() == "jpg" else ext.lower()
 
     # Set resolution based on preset resolutions
-    if resolution in ["prev", "preview"]:
+    if resolution in ("prev", "preview"):
         res_x, res_y = (1920, 1080)
-    elif resolution in ["thumb", "thumbnail"]:
-        res_x, res_y = (400, 400)
-    elif resolution in ["pfp", "profile"]:
-        res_x, res_y = (200, 200)
-    elif resolution in ["icon", "favicon"]:
-        res_x, res_y = (25, 25)
+    elif resolution in ("thumb", "thumbnail"):
+        res_x, res_y = (300, 300)
+    elif resolution in ("pfp", "profile"):
+        res_x, res_y = (150, 150)
+    elif resolution in ("icon", "favicon"):
+        res_x, res_y = (30, 30)
     else:
         return None
 
+    cache_file_name = "{}_{}x{}.{}".format(file_name, res_x, res_y, ext).lower()
+
     # If image has been already generated, return it from the cache
-    if os.path.exists(os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}")):
-        return os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}")
+    if os.path.exists(os.path.join(CACHE_FOLDER, cache_file_name)):
+        return os.path.join(CACHE_FOLDER, cache_file_name)
 
     # Check if image exists in the uploads directory
     if not os.path.exists(os.path.join(MEDIA_FOLDER, file_path)):
@@ -61,7 +61,7 @@ def generate_thumbnail(file_path, resolution, ext=None):
     # Save image to cache directory
     try:
         image.save(
-            os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}"),
+            os.path.join(CACHE_FOLDER, cache_file_name),
             icc_profile=image_icc,
         )
     except OSError:
@@ -69,11 +69,11 @@ def generate_thumbnail(file_path, resolution, ext=None):
         # so we convert to RGB and try again
         image = image.convert("RGB")
         image.save(
-            os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}"),
+            os.path.join(CACHE_FOLDER, cache_file_name),
             icc_profile=image_icc,
         )
 
     # No need to keep the image in memory, learned the hard way
     image.close()
 
-    return os.path.join(CACHE_FOLDER, f"{file_name}_{res_x}x{res_y}.{ext}")
+    return os.path.join(CACHE_FOLDER, cache_file_name)
diff --git a/onlylegs/utils/metadata/__init__.py b/onlylegs/utils/metadata/__init__.py
index 302116a..ace9e76 100644
--- a/onlylegs/utils/metadata/__init__.py
+++ b/onlylegs/utils/metadata/__init__.py
@@ -12,90 +12,74 @@ from .helpers import *
 from .mapping import *
 
 
-class Metadata:
+def yoink(file_path):
     """
-    Metadata parser
+    Initialize the metadata parser
     """
+    if not os.path.isfile(file_path):
+        return None
 
-    def __init__(self, file_path):
-        """
-        Initialize the metadata parser
-        """
-        self.file_path = file_path
-        img_exif = {}
+    img_exif = {}
+    file = Image.open(file_path)
 
-        try:
-            file = Image.open(file_path)
-            tags = file._getexif()
-            img_exif = {}
+    img_exif["FileName"] = os.path.basename(file_path)
+    img_exif["FileSize"] = os.path.getsize(file_path)
+    img_exif["FileFormat"] = img_exif["FileName"].split(".")[-1]
+    img_exif["FileWidth"], img_exif["FileHeight"] = file.size
 
-            for tag, value in TAGS.items():
-                if tag in tags:
-                    img_exif[value] = tags[tag]
+    try:
+        tags = file._getexif()
+        for tag, value in TAGS.items():
+            if tag in tags:
+                img_exif[value] = tags[tag]
+    except TypeError:
+        pass
 
-            img_exif["FileName"] = os.path.basename(file_path)
-            img_exif["FileSize"] = os.path.getsize(file_path)
-            img_exif["FileFormat"] = img_exif["FileName"].split(".")[-1]
-            img_exif["FileWidth"], img_exif["FileHeight"] = file.size
+    file.close()
 
-            file.close()
-        except TypeError:
-            img_exif["FileName"] = os.path.basename(file_path)
-            img_exif["FileSize"] = os.path.getsize(file_path)
-            img_exif["FileFormat"] = img_exif["FileName"].split(".")[-1]
-            img_exif["FileWidth"], img_exif["FileHeight"] = file.size
+    return _format_data(img_exif)
 
-        self.encoded = img_exif
 
-    def yoink(self):
-        """
-        Yoinks the metadata from the image
-        """
-        if not os.path.isfile(self.file_path):
-            return None
-        return self.format_data(self.encoded)
+def _format_data(encoded):
+    """
+    Formats the data into a dictionary
+    """
+    exif = {
+        "Photographer": {},
+        "Camera": {},
+        "Software": {},
+        "File": {},
+    }
 
-    @staticmethod
-    def format_data(encoded_exif):
-        """
-        Formats the data into a dictionary
-        """
-        exif = {
-            "Photographer": {},
-            "Camera": {},
-            "Software": {},
-            "File": {},
-        }
+    # Thanks chatGPT xP
+    # the helper function works, so not sure why it triggers pylint
+    for key, value in encoded.items():
+        for mapping_name, mapping_val in EXIF_MAPPING:
+            if key in mapping_val:
+                if len(mapping_val[key]) == 2:
+                    exif[mapping_name][mapping_val[key][0]] = {
+                        "raw": value,
+                        "formatted": (
+                            getattr(
+                                helpers,  # pylint: disable=E0602
+                                mapping_val[key][1],
+                            )(value)
+                        ),
+                    }
+                else:
+                    exif[mapping_name][mapping_val[key][0]] = {
+                        "raw": value,
+                    }
+                continue
 
-        # Thanks chatGPT xP
-        # the helper function works, so not sure why it triggers pylint
-        for key, value in encoded_exif.items():
-            for mapping_name, mapping_val in EXIF_MAPPING:
-                if key in mapping_val:
-                    if len(mapping_val[key]) == 2:
-                        exif[mapping_name][mapping_val[key][0]] = {
-                            "raw": value,
-                            "formatted": (
-                                getattr(
-                                    helpers,  # pylint: disable=E0602
-                                    mapping_val[key][1],
-                                )(value)
-                            ),
-                        }
-                    else:
-                        exif[mapping_name][mapping_val[key][0]] = {
-                            "raw": value,
-                        }
-                    continue
+    # Remove empty keys
+    if not exif["Photographer"]:
+        del exif["Photographer"]
+    if not exif["Camera"]:
+        del exif["Camera"]
+    if not exif["Software"]:
+        del exif["Software"]
+    if not exif["File"]:
+        del exif["File"]
 
-        # Remove empty keys
-        if not exif["Photographer"]:
-            del exif["Photographer"]
-        if not exif["Camera"]:
-            del exif["Camera"]
-        if not exif["Software"]:
-            del exif["Software"]
-        if not exif["File"]:
-            del exif["File"]
-
-        return exif
+    return exif
diff --git a/onlylegs/views/group.py b/onlylegs/views/group.py
index 7fe1bdc..56f39f8 100644
--- a/onlylegs/views/group.py
+++ b/onlylegs/views/group.py
@@ -3,11 +3,11 @@ Onlylegs - Image Groups
 Why groups? Because I don't like calling these albums
 sounds more limiting that it actually is in this gallery
 """
-from flask import Blueprint, render_template, url_for
-
-from onlylegs.models import Post, User, GroupJunction, Group
+from flask import Blueprint, render_template, url_for, request, flash, jsonify
+from flask_login import login_required, current_user
+from onlylegs.models import Pictures, Users, AlbumJunction, Albums
 from onlylegs.extensions import db
-from onlylegs.utils import contrast
+from onlylegs.utils import colour
 
 
 blueprint = Blueprint("group", __name__, url_prefix="/group")
@@ -18,21 +18,21 @@ def groups():
     """
     Group overview, shows all image groups
     """
-    groups = Group.query.all()
+    groups = Albums.query.all()
 
     # For each group, get the 3 most recent images
     for group in groups:
         group.author_username = (
-            User.query.with_entities(User.username)
-            .filter(User.id == group.author_id)
+            Users.query.with_entities(Users.username)
+            .filter(Users.id == group.author_id)
             .first()[0]
         )
 
         # Get the 3 most recent images
         images = (
-            GroupJunction.query.with_entities(GroupJunction.post_id)
-            .filter(GroupJunction.group_id == group.id)
-            .order_by(GroupJunction.date_added.desc())
+            AlbumJunction.query.with_entities(AlbumJunction.picture_id)
+            .filter(AlbumJunction.album_id == group.id)
+            .order_by(AlbumJunction.date_added.desc())
             .limit(3)
         )
 
@@ -40,40 +40,67 @@ def groups():
         group.images = []
         for image in images:
             group.images.append(
-                Post.query.with_entities(Post.filename, Post.alt, Post.colours, Post.id)
-                .filter(Post.id == image[0])
+                Pictures.query.with_entities(
+                    Pictures.filename, Pictures.alt, Pictures.colours, Pictures.id
+                )
+                .filter(Pictures.id == image[0])
                 .first()
             )
 
     return render_template("list.html", groups=groups)
 
 
-@blueprint.route("/<int:group_id>")
+@blueprint.route("/", methods=["POST"])
+@login_required
+def groups_post():
+    """
+    Creates a group
+    """
+    group_name = request.form.get("name", "").strip()
+    group_description = request.form.get("description", "").strip()
+
+    new_group = Albums(
+        name=group_name,
+        description=group_description,
+        author_id=current_user.id,
+    )
+
+    db.session.add(new_group)
+    db.session.commit()
+
+    flash(["Group created!", "1"])
+    return jsonify({"message": "Group created", "id": new_group.id})
+
+
+@blueprint.route("/<int:group_id>", methods=["GET"])
 def group(group_id):
     """
     Group view, shows all images in a group
     """
     # Get the group, if it doesn't exist, 404
-    group = db.get_or_404(Group, group_id, description="Group not found! D:")
+    group = db.get_or_404(Albums, group_id, description="Group not found! D:")
 
     # Get all images in the group from the junction table
     junction = (
-        GroupJunction.query.with_entities(GroupJunction.post_id)
-        .filter(GroupJunction.group_id == group_id)
-        .order_by(GroupJunction.date_added.desc())
+        AlbumJunction.query.with_entities(AlbumJunction.picture_id)
+        .filter(AlbumJunction.album_id == group_id)
+        .order_by(AlbumJunction.date_added.desc())
         .all()
     )
 
     # Get the image data for each image in the group
     images = []
     for image in junction:
-        images.append(Post.query.filter(Post.id == image[0]).first())
+        images.append(Pictures.query.filter(Pictures.id == image[0]).first())
 
     # Check contrast for the first image in the group for the banner
     text_colour = "rgb(var(--fg-black))"
     if images:
-        text_colour = contrast.contrast(
-            images[0].colours[0], "rgb(var(--fg-black))", "rgb(var(--fg-white))"
+        colour_obj = colour.Colour(images[0].colours[0])
+        text_colour = (
+            "rgb(var(--fg-black));"
+            if colour_obj.is_light()
+            else "rgb(var(--fg-white));"
         )
 
     return render_template(
@@ -81,18 +108,66 @@ def group(group_id):
     )
 
 
+@blueprint.route("/<int:group_id>", methods=["PUT"])
+@login_required
+def group_put(group_id):
+    """
+    Changes the images in a group
+    """
+    image_id = request.form.get("imageId", "").strip()
+    action = request.form.get("action", "").strip()
+
+    group_record = db.get_or_404(Albums, group_id)
+    db.get_or_404(Pictures, image_id)  # Check if image exists
+
+    if group_record.author_id != current_user.id:
+        return jsonify({"message": "You are not the owner of this group"}), 403
+
+    junction_exist = AlbumJunction.query.filter_by(
+        album_id=group_id, picture_id=image_id
+    ).first()
+
+    if action == "add" and not junction_exist:
+        db.session.add(AlbumJunction(album_id=group_id, picture_id=image_id))
+    elif request.form["action"] == "remove":
+        AlbumJunction.query.filter_by(album_id=group_id, picture_id=image_id).delete()
+
+    db.session.commit()
+    flash(["Group modified!", "1"])
+    return jsonify({"message": "Group modified"})
+
+
+@blueprint.route("/<int:group_id>", methods=["DELETE"])
+@login_required
+def group_delete(group_id):
+    """
+    Deletes a group
+    """
+    group_record = db.get_or_404(Albums, group_id)
+
+    if group_record.author_id != current_user.id:
+        return jsonify({"message": "You are not the owner of this group"}), 403
+
+    AlbumJunction.query.filter_by(album_id=group_id).delete()
+    db.session.delete(group_record)
+    db.session.commit()
+
+    flash(["Group yeeted!", "1"])
+    return jsonify({"message": "Group deleted"})
+
+
 @blueprint.route("/<int:group_id>/<int:image_id>")
 def group_post(group_id, image_id):
     """
     Image view, shows the image and its metadata from a specific group
     """
     # Get the image, if it doesn't exist, 404
-    image = db.get_or_404(Post, image_id, description="Image not found :<")
+    image = db.get_or_404(Pictures, image_id, description="Image not found :<")
 
     # Get all groups the image is in
     groups = (
-        GroupJunction.query.with_entities(GroupJunction.group_id)
-        .filter(GroupJunction.post_id == image_id)
+        AlbumJunction.query.with_entities(AlbumJunction.album_id)
+        .filter(AlbumJunction.picture_id == image_id)
         .all()
     )
 
@@ -100,24 +175,24 @@ def group_post(group_id, image_id):
     image.groups = []
     for group in groups:
         image.groups.append(
-            Group.query.with_entities(Group.id, Group.name)
-            .filter(Group.id == group[0])
+            Albums.query.with_entities(Albums.id, Albums.name)
+            .filter(Albums.id == group[0])
             .first()
         )
 
     # Get the next and previous images in the group
     next_url = (
-        GroupJunction.query.with_entities(GroupJunction.post_id)
-        .filter(GroupJunction.group_id == group_id)
-        .filter(GroupJunction.post_id > image_id)
-        .order_by(GroupJunction.date_added.asc())
+        AlbumJunction.query.with_entities(AlbumJunction.picture_id)
+        .filter(AlbumJunction.album_id == group_id)
+        .filter(AlbumJunction.picture_id > image_id)
+        .order_by(AlbumJunction.date_added.asc())
         .first()
     )
     prev_url = (
-        GroupJunction.query.with_entities(GroupJunction.post_id)
-        .filter(GroupJunction.group_id == group_id)
-        .filter(GroupJunction.post_id < image_id)
-        .order_by(GroupJunction.date_added.desc())
+        AlbumJunction.query.with_entities(AlbumJunction.picture_id)
+        .filter(AlbumJunction.album_id == group_id)
+        .filter(AlbumJunction.picture_id < image_id)
+        .order_by(AlbumJunction.date_added.desc())
         .first()
     )
 
@@ -127,6 +202,14 @@ def group_post(group_id, image_id):
     if prev_url:
         prev_url = url_for("group.group_post", group_id=group_id, image_id=prev_url[0])
 
+    close_tab = True
+    if request.cookies.get("image-info") == "0":
+        close_tab = False
+
     return render_template(
-        "image.html", image=image, next_url=next_url, prev_url=prev_url
+        "image.html",
+        image=image,
+        next_url=next_url,
+        prev_url=prev_url,
+        close_tab=close_tab,
     )
diff --git a/onlylegs/views/image.py b/onlylegs/views/image.py
index a192f15..75a70fd 100644
--- a/onlylegs/views/image.py
+++ b/onlylegs/views/image.py
@@ -1,27 +1,39 @@
 """
 Onlylegs - Image View
 """
+import os
+import logging
+import pathlib
 from math import ceil
-from flask import Blueprint, render_template, url_for, current_app
-from onlylegs.models import Post, GroupJunction, Group
+from flask import (
+    Blueprint,
+    render_template,
+    url_for,
+    current_app,
+    request,
+    flash,
+    jsonify,
+)
+from flask_login import current_user
+from onlylegs.models import Pictures, AlbumJunction, Albums
 from onlylegs.extensions import db
 
 
 blueprint = Blueprint("image", __name__, url_prefix="/image")
 
 
-@blueprint.route("/<int:image_id>")
+@blueprint.route("/<int:image_id>", methods=["GET"])
 def image(image_id):
     """
     Image view, shows the image and its metadata
     """
     # Get the image, if it doesn't exist, 404
-    image = db.get_or_404(Post, image_id, description="Image not found :<")
+    image = db.get_or_404(Pictures, image_id, description="Image not found :<")
 
     # Get all groups the image is in
     groups = (
-        GroupJunction.query.with_entities(GroupJunction.group_id)
-        .filter(GroupJunction.post_id == image_id)
+        AlbumJunction.query.with_entities(AlbumJunction.album_id)
+        .filter(AlbumJunction.picture_id == image_id)
         .all()
     )
 
@@ -29,34 +41,34 @@ def image(image_id):
     image.groups = []
     for group in groups:
         image.groups.append(
-            Group.query.with_entities(Group.id, Group.name)
-            .filter(Group.id == group[0])
+            Albums.query.with_entities(Albums.id, Albums.name)
+            .filter(Albums.id == group[0])
             .first()
         )
 
     # Get the next and previous images
     # Check if there is a group ID set
     next_url = (
-        Post.query.with_entities(Post.id)
-        .filter(Post.id > image_id)
-        .order_by(Post.id.asc())
+        Pictures.query.with_entities(Pictures.id)
+        .filter(Pictures.id > image_id)
+        .order_by(Pictures.id.asc())
         .first()
     )
     prev_url = (
-        Post.query.with_entities(Post.id)
-        .filter(Post.id < image_id)
-        .order_by(Post.id.desc())
+        Pictures.query.with_entities(Pictures.id)
+        .filter(Pictures.id < image_id)
+        .order_by(Pictures.id.desc())
         .first()
     )
 
     # If there is a next or previous image, get the url
-    if next_url:
-        next_url = url_for("image.image", image_id=next_url[0])
-    if prev_url:
-        prev_url = url_for("image.image", image_id=prev_url[0])
+    next_url = url_for("image.image", image_id=next_url[0]) if next_url else None
+    prev_url = url_for("image.image", image_id=prev_url[0]) if prev_url else None
 
     # Yoink all the images in the database
-    total_images = Post.query.with_entities(Post.id).order_by(Post.id.desc()).all()
+    total_images = (
+        Pictures.query.with_entities(Pictures.id).order_by(Pictures.id.desc()).all()
+    )
     limit = current_app.config["UPLOAD_CONF"]["max-load"]
 
     # If the number of items is less than the limit, no point of calculating the page
@@ -72,10 +84,72 @@ def image(image_id):
                     return_page = i + 1
                     break
 
+    close_tab = True
+    if request.cookies.get("image-info") == "0":
+        close_tab = False
+
     return render_template(
         "image.html",
         image=image,
         next_url=next_url,
         prev_url=prev_url,
         return_page=return_page,
+        close_tab=close_tab,
     )
+
+
+@blueprint.route("/<int:image_id>", methods=["PUT"])
+def image_put(image_id):
+    """
+    Update the image metadata
+    """
+    image_record = db.get_or_404(Pictures, image_id, description="Image not found :<")
+
+    image_record.description = request.form.get("description", image_record.description)
+    image_record.alt = request.form.get("alt", image_record.alt)
+
+    print(request.form.get("description"))
+
+    db.session.commit()
+
+    flash(["Image updated!", "1"])
+    return "OK", 200
+
+
+@blueprint.route("/<int:image_id>", methods=["DELETE"])
+def image_delete(image_id):
+    image_record = db.get_or_404(Pictures, image_id)
+
+    # Check if image exists and if user is allowed to delete it (author)
+    if image_record.author_id != current_user.id:
+        logging.info("User %s tried to delete image %s", current_user.id, image_id)
+        return (
+            jsonify({"message": "You are not allowed to delete this image, heck off"}),
+            403,
+        )
+
+    # Delete file
+    try:
+        os.remove(
+            os.path.join(current_app.config["UPLOAD_FOLDER"], image_record.filename)
+        )
+    except FileNotFoundError:
+        logging.warning(
+            "File not found: %s, already deleted or never existed",
+            image_record.filename,
+        )
+
+    # Delete cached files
+    cache_name = image_record.filename.rsplit(".")[0]
+    for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob(
+        cache_name + "*"
+    ):
+        os.remove(cache_file)
+
+    AlbumJunction.query.filter_by(picture_id=image_id).delete()
+    db.session.delete(image_record)
+    db.session.commit()
+
+    logging.info("Removed image (%s) %s", image_id, image_record.filename)
+    flash(["Image was all in Le Head!", "1"])
+    return jsonify({"message": "Image deleted"}), 200
diff --git a/onlylegs/views/index.py b/onlylegs/views/index.py
index 3cc0155..943507a 100644
--- a/onlylegs/views/index.py
+++ b/onlylegs/views/index.py
@@ -2,11 +2,9 @@
 Onlylegs Gallery - Index view
 """
 from math import ceil
-
 from flask import Blueprint, render_template, request, current_app
 from werkzeug.exceptions import abort
-
-from onlylegs.models import Post
+from onlylegs.models import Pictures, Users
 
 
 blueprint = Blueprint("gallery", __name__)
@@ -17,31 +15,32 @@ def index():
     """
     Home page of the website, shows the feed of the latest images
     """
-    # meme
-    if request.args.get("coffee") == "please":
-        abort(418)
-
     # pagination, defaults to page 1 if no page is specified
     page = request.args.get("page", default=1, type=int)
     limit = current_app.config["UPLOAD_CONF"]["max-load"]
 
     # get the total number of images in the database
     # calculate the total number of pages, and make sure the page number is valid
-    total_images = Post.query.with_entities(Post.id).count()
+    total_images = Pictures.query.with_entities(Pictures.id).count()
     pages = ceil(max(total_images, limit) / limit)
     if page > pages:
-        abort(
+        return abort(
             404,
-            "You have reached the far and beyond, "
-            + "but you will not find your answers here.",
+            "You have reached the far and beyond, but you will not find your answers here.",
         )
 
     # get the images for the current page
     images = (
-        Post.query.with_entities(
-            Post.filename, Post.alt, Post.colours, Post.created_at, Post.id
+        Pictures.query.with_entities(
+            Pictures.filename,
+            Pictures.alt,
+            Pictures.colours,
+            Pictures.created_at,
+            Pictures.id,
+            Users.username,
         )
-        .order_by(Post.id.desc())
+        .join(Users)
+        .order_by(Pictures.id.desc())
         .offset((page - 1) * limit)
         .limit(limit)
         .all()
diff --git a/onlylegs/views/profile.py b/onlylegs/views/profile.py
index b83399f..22457d7 100644
--- a/onlylegs/views/profile.py
+++ b/onlylegs/views/profile.py
@@ -5,7 +5,7 @@ from flask import Blueprint, render_template, request
 from werkzeug.exceptions import abort
 from flask_login import current_user
 
-from onlylegs.models import Post, User, Group
+from onlylegs.models import Pictures, Users, Albums
 from onlylegs.extensions import db
 
 
@@ -27,9 +27,9 @@ def profile():
             abort(404, "You must be logged in to view your own profile!")
 
     # Get the user's data
-    user = db.get_or_404(User, user_id, description="User not found :<")
+    user = db.get_or_404(Users, user_id, description="User not found :<")
 
-    images = Post.query.filter(Post.author_id == user_id).all()
-    groups = Group.query.filter(Group.author_id == user_id).all()
+    images = Pictures.query.filter(Pictures.author_id == user_id).all()
+    groups = Albums.query.filter(Albums.author_id == user_id).all()
 
     return render_template("profile.html", user=user, images=images, groups=groups)
diff --git a/poetry.lock b/poetry.lock
index ff854af..8c1c926 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2,14 +2,14 @@
 
 [[package]]
 name = "alembic"
-version = "1.10.3"
+version = "1.11.2"
 description = "A database migration tool for SQLAlchemy."
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "alembic-1.10.3-py3-none-any.whl", hash = "sha256:b2e0a6cfd3a8ce936a1168320bcbe94aefa3f4463cd773a968a55071beb3cd37"},
-    {file = "alembic-1.10.3.tar.gz", hash = "sha256:32a69b13a613aeb7e8093f242da60eff9daed13c0df02fff279c1b06c32965d2"},
+    {file = "alembic-1.11.2-py3-none-any.whl", hash = "sha256:7981ab0c4fad4fe1be0cf183aae17689fe394ff874fd2464adb774396faf0796"},
+    {file = "alembic-1.11.2.tar.gz", hash = "sha256:678f662130dc540dac12de0ea73de9f89caea9dbea138f60ef6263149bf84657"},
 ]
 
 [package.dependencies]
@@ -24,14 +24,14 @@ tz = ["python-dateutil"]
 
 [[package]]
 name = "astroid"
-version = "2.15.3"
+version = "2.15.6"
 description = "An abstract syntax tree for Python with inference support."
 category = "main"
 optional = false
 python-versions = ">=3.7.2"
 files = [
-    {file = "astroid-2.15.3-py3-none-any.whl", hash = "sha256:f11e74658da0f2a14a8d19776a8647900870a63de71db83713a8e77a6af52662"},
-    {file = "astroid-2.15.3.tar.gz", hash = "sha256:44224ad27c54d770233751315fa7f74c46fa3ee0fab7beef1065f99f09897efe"},
+    {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"},
+    {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"},
 ]
 
 [package.dependencies]
@@ -44,37 +44,34 @@ wrapt = [
 
 [[package]]
 name = "black"
-version = "23.3.0"
+version = "23.7.0"
 description = "The uncompromising code formatter."
 category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"},
-    {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"},
-    {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"},
-    {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"},
-    {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"},
-    {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"},
-    {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"},
-    {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"},
-    {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"},
-    {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"},
-    {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"},
-    {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"},
-    {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"},
-    {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"},
-    {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"},
-    {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"},
-    {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"},
-    {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"},
-    {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"},
-    {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"},
-    {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"},
-    {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"},
-    {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"},
-    {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"},
-    {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"},
+    {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"},
+    {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"},
+    {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"},
+    {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"},
+    {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"},
+    {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"},
+    {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"},
+    {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"},
+    {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"},
+    {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"},
+    {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"},
+    {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"},
+    {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"},
+    {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"},
+    {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"},
+    {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"},
+    {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"},
+    {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"},
+    {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"},
+    {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"},
+    {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"},
+    {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"},
 ]
 
 [package.dependencies]
@@ -197,27 +194,27 @@ files = [
 ]
 
 [[package]]
-name = "cachelib"
-version = "0.9.0"
-description = "A collection of cache libraries in the same API interface."
+name = "cachetools"
+version = "5.3.1"
+description = "Extensible memoizing collections and decorators"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "cachelib-0.9.0-py3-none-any.whl", hash = "sha256:811ceeb1209d2fe51cd2b62810bd1eccf70feba5c52641532498be5c675493b3"},
-    {file = "cachelib-0.9.0.tar.gz", hash = "sha256:38222cc7c1b79a23606de5c2607f4925779e37cdcea1c2ad21b8bae94b5425a5"},
+    {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"},
+    {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"},
 ]
 
 [[package]]
 name = "click"
-version = "8.1.3"
+version = "8.1.6"
 description = "Composable command line interface toolkit"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
-    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
+    {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"},
+    {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"},
 ]
 
 [package.dependencies]
@@ -263,14 +260,14 @@ files = [
 
 [[package]]
 name = "dill"
-version = "0.3.6"
-description = "serialize all of python"
+version = "0.3.7"
+description = "serialize all of Python"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"},
-    {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"},
+    {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"},
+    {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"},
 ]
 
 [package.extras]
@@ -318,19 +315,18 @@ webassets = ">=2.0"
 
 [[package]]
 name = "flask-caching"
-version = "2.0.2"
-description = "Adds caching support to Flask applications."
+version = "1.10.1"
+description = "Adds caching support to your Flask application"
 category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.5"
 files = [
-    {file = "Flask-Caching-2.0.2.tar.gz", hash = "sha256:24b60c552d59a9605cc1b6a42c56cdb39a82a28dab4532bbedb9222ae54ecb4e"},
-    {file = "Flask_Caching-2.0.2-py3-none-any.whl", hash = "sha256:19571f2570e9b8dd9dd9d2f49d7cbee69c14ebe8cc001100b1eb98c379dd80ad"},
+    {file = "Flask-Caching-1.10.1.tar.gz", hash = "sha256:cf19b722fcebc2ba03e4ae7c55b532ed53f0cbf683ce36fafe5e881789a01c00"},
+    {file = "Flask_Caching-1.10.1-py3-none-any.whl", hash = "sha256:bcda8acbc7508e31e50f63e9b1ab83185b446f6b6318bd9dd1d45626fba2e903"},
 ]
 
 [package.dependencies]
-cachelib = ">=0.9.0,<0.10.0"
-Flask = "<3"
+Flask = "*"
 
 [[package]]
 name = "flask-compress"
@@ -383,19 +379,19 @@ Flask-SQLAlchemy = ">=1.0"
 
 [[package]]
 name = "flask-sqlalchemy"
-version = "3.0.3"
+version = "3.0.5"
 description = "Add SQLAlchemy support to your Flask application."
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "Flask-SQLAlchemy-3.0.3.tar.gz", hash = "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec"},
-    {file = "Flask_SQLAlchemy-3.0.3-py3-none-any.whl", hash = "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a"},
+    {file = "flask_sqlalchemy-3.0.5-py3-none-any.whl", hash = "sha256:cabb6600ddd819a9f859f36515bb1bd8e7dbf30206cc679d2b081dff9e383283"},
+    {file = "flask_sqlalchemy-3.0.5.tar.gz", hash = "sha256:c5765e58ca145401b52106c0f46178569243c5da25556be2c231ecc60867c5b1"},
 ]
 
 [package.dependencies]
-Flask = ">=2.2"
-SQLAlchemy = ">=1.4.18"
+flask = ">=2.2.5"
+sqlalchemy = ">=1.4.18"
 
 [[package]]
 name = "greenlet"
@@ -494,14 +490,14 @@ tornado = ["tornado (>=0.2)"]
 
 [[package]]
 name = "importlib-metadata"
-version = "6.5.0"
+version = "6.8.0"
 description = "Read metadata from Python packages"
 category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "importlib_metadata-6.5.0-py3-none-any.whl", hash = "sha256:03ba783c3a2c69d751b109fc0c94a62c51f581b3d6acf8ed1331b6d5729321ff"},
-    {file = "importlib_metadata-6.5.0.tar.gz", hash = "sha256:7a8bdf1bc3a726297f5cfbc999e6e7ff6b4fa41b26bba4afc580448624460045"},
+    {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"},
+    {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"},
 ]
 
 [package.dependencies]
@@ -510,26 +506,26 @@ zipp = ">=0.5"
 [package.extras]
 docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
 perf = ["ipython"]
-testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
+testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
 
 [[package]]
 name = "importlib-resources"
-version = "5.12.0"
+version = "6.0.0"
 description = "Read resources from Python packages"
 category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"},
-    {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"},
+    {file = "importlib_resources-6.0.0-py3-none-any.whl", hash = "sha256:d952faee11004c045f785bb5636e8f885bed30dc3c940d5d42798a2a4541c185"},
+    {file = "importlib_resources-6.0.0.tar.gz", hash = "sha256:4cf94875a8368bd89531a756df9a9ebe1f150e0f885030b461237bc7f2d905f2"},
 ]
 
 [package.dependencies]
 zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
 
 [[package]]
 name = "isort"
@@ -673,62 +669,62 @@ testing = ["pytest"]
 
 [[package]]
 name = "markupsafe"
-version = "2.1.2"
+version = "2.1.3"
 description = "Safely add untrusted strings to HTML/XML markup."
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
-    {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
-    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
-    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
-    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
-    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
-    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
-    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
-    {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
-    {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
-    {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
-    {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
-    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
-    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
-    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
-    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
-    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
-    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
-    {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
-    {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
-    {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
-    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
-    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
-    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
-    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
-    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
-    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
-    {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
-    {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
-    {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
-    {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
-    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
-    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
-    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
-    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
-    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
-    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
-    {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
-    {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
-    {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
-    {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
-    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
-    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
-    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
-    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
-    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
-    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
-    {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
-    {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
-    {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
+    {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
+    {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
+    {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
+    {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
+    {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
+    {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
+    {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
+    {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
+    {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
+    {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
+    {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
+    {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
+    {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
+    {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
+    {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
+    {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
+    {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
+    {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
+    {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
+    {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
+    {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
+    {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
+    {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
+    {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
+    {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
+    {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
+    {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
+    {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
+    {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
+    {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
+    {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
+    {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
+    {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
+    {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
+    {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
+    {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
+    {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
+    {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
+    {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
+    {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
+    {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
+    {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
+    {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
+    {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
+    {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
+    {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
+    {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
+    {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
+    {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
+    {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
 ]
 
 [[package]]
@@ -769,14 +765,14 @@ files = [
 
 [[package]]
 name = "pathspec"
-version = "0.11.1"
+version = "0.11.2"
 description = "Utility library for gitignore style pattern matching of file paths."
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
-    {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
+    {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
+    {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
 ]
 
 [[package]]
@@ -861,34 +857,34 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
 
 [[package]]
 name = "platformdirs"
-version = "3.2.0"
+version = "3.10.0"
 description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"},
-    {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"},
+    {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"},
+    {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"},
 ]
 
 [package.extras]
-docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
-test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
 
 [[package]]
 name = "pylint"
-version = "2.17.2"
+version = "2.17.5"
 description = "python code static checker"
 category = "main"
 optional = false
 python-versions = ">=3.7.2"
 files = [
-    {file = "pylint-2.17.2-py3-none-any.whl", hash = "sha256:001cc91366a7df2970941d7e6bbefcbf98694e00102c1f121c531a814ddc2ea8"},
-    {file = "pylint-2.17.2.tar.gz", hash = "sha256:1b647da5249e7c279118f657ca28b6aaebb299f86bf92affc632acf199f7adbb"},
+    {file = "pylint-2.17.5-py3-none-any.whl", hash = "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413"},
+    {file = "pylint-2.17.5.tar.gz", hash = "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"},
 ]
 
 [package.dependencies]
-astroid = ">=2.15.2,<=2.17.0-dev0"
+astroid = ">=2.15.6,<=2.17.0-dev0"
 colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
 dill = [
     {version = ">=0.2", markers = "python_version < \"3.11\""},
@@ -922,120 +918,120 @@ cli = ["click (>=5.0)"]
 
 [[package]]
 name = "pyyaml"
-version = "6.0"
+version = "6.0.1"
 description = "YAML parser and emitter for Python"
 category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
-    {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
-    {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
-    {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
-    {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
-    {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
-    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
-    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
-    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
-    {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
-    {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
-    {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
-    {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
-    {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
-    {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
-    {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
-    {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
-    {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
-    {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
-    {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
-    {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
-    {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
-    {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
-    {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
-    {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+    {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+    {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
 ]
 
 [[package]]
 name = "setuptools"
-version = "67.7.0"
+version = "68.0.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "setuptools-67.7.0-py3-none-any.whl", hash = "sha256:888be97fde8cc3afd60f7784e678fa29ee13c4e5362daa7104a93bba33646c50"},
-    {file = "setuptools-67.7.0.tar.gz", hash = "sha256:b7e53a01c6c654d26d2999ee033d8c6125e5fa55f03b7b193f937ae7ac999f22"},
+    {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"},
+    {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"},
 ]
 
 [package.extras]
 docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
 testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
 
 [[package]]
 name = "sqlalchemy"
-version = "2.0.9"
+version = "2.0.19"
 description = "Database Abstraction Library"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:734805708632e3965c2c40081f9a59263c29ffa27cba9b02d4d92dfd57ba869f"},
-    {file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d3ece5960b3e821e43a4927cc851b6e84a431976d3ffe02aadb96519044807e"},
-    {file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d118e233f416d713aac715e2c1101e17f91e696ff315fc9efbc75b70d11e740"},
-    {file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f005245e1cb9b8ca53df73ee85e029ac43155e062405015e49ec6187a2e3fb44"},
-    {file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:34eb96c1de91d8f31e988302243357bef3f7785e1b728c7d4b98bd0c117dafeb"},
-    {file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7e472e9627882f2d75b87ff91c5a2bc45b31a226efc7cc0a054a94fffef85862"},
-    {file = "SQLAlchemy-2.0.9-cp310-cp310-win32.whl", hash = "sha256:0a865b5ec4ba24f57c33b633b728e43fde77b968911a6046443f581b25d29dd9"},
-    {file = "SQLAlchemy-2.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:6e84ab63d25d8564d7a8c05dc080659931a459ee27f6ed1cf4c91f292d184038"},
-    {file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db4bd1c4792da753f914ff0b688086b9a8fd78bb9bc5ae8b6d2e65f176b81eb9"},
-    {file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad5363a1c65fde7b7466769d4261126d07d872fc2e816487ae6cec93da604b6b"},
-    {file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebc4eeb1737a5a9bdb0c24f4c982319fa6edd23cdee27180978c29cbb026f2bd"},
-    {file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbda1da8d541904ba262825a833c9f619e93cb3fd1156be0a5e43cd54d588dcd"},
-    {file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d5327f54a9c39e7871fc532639616f3777304364a0bb9b89d6033ad34ef6c5f8"},
-    {file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ac6a0311fb21a99855953f84c43fcff4bdca27a2ffcc4f4d806b26b54b5cddc9"},
-    {file = "SQLAlchemy-2.0.9-cp311-cp311-win32.whl", hash = "sha256:d209594e68bec103ad5243ecac1b40bf5770c9ebf482df7abf175748a34f4853"},
-    {file = "SQLAlchemy-2.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:865392a50a721445156809c1a6d6ab6437be70c1c2599f591a8849ed95d3c693"},
-    {file = "SQLAlchemy-2.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0b49f1f71d7a44329a43d3edd38cc5ee4c058dfef4487498393d16172007954b"},
-    {file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a019f723b6c1e6b3781be00fb9e0844bc6156f9951c836ff60787cc3938d76"},
-    {file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9838bd247ee42eb74193d865e48dd62eb50e45e3fdceb0fdef3351133ee53dcf"},
-    {file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:78612edf4ba50d407d0eb3a64e9ec76e6efc2b5d9a5c63415d53e540266a230a"},
-    {file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f61ab84956dc628c8dfe9d105b6aec38afb96adae3e5e7da6085b583ff6ea789"},
-    {file = "SQLAlchemy-2.0.9-cp37-cp37m-win32.whl", hash = "sha256:07950fc82f844a2de67ddb4e535f29b65652b4d95e8b847823ce66a6d540a41d"},
-    {file = "SQLAlchemy-2.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:e62c4e762d6fd2901692a093f208a6a6575b930e9458ad58c2a7f080dd6132da"},
-    {file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b3e5864eba71a3718236a120547e52c8da2ccb57cc96cecd0480106a0c799c92"},
-    {file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d06e119cf79a3d80ab069f064a07152eb9ba541d084bdaee728d8a6f03fd03d"},
-    {file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee2946042cc7851842d7a086a92b9b7b494cbe8c3e7e4627e27bc912d3a7655e"},
-    {file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13f984a190d249769a050634b248aef8991acc035e849d02b634ea006c028fa8"},
-    {file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e4780be0f19e5894c17f75fc8de2fe1ae233ab37827125239ceb593c6f6bd1e2"},
-    {file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:68ed381bc340b4a3d373dbfec1a8b971f6350139590c4ca3cb722fdb50035777"},
-    {file = "SQLAlchemy-2.0.9-cp38-cp38-win32.whl", hash = "sha256:aa5c270ece17c0c0e0a38f2530c16b20ea05d8b794e46c79171a86b93b758891"},
-    {file = "SQLAlchemy-2.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:1b69666e25cc03c602d9d3d460e1281810109e6546739187044fc256c67941ef"},
-    {file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6e27189ff9aebfb2c02fd252c629ea58657e7a5ff1a321b7fc9c2bf6dc0b5f3"},
-    {file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8239ce63a90007bce479adf5460d48c1adae4b933d8e39a4eafecfc084e503c"},
-    {file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f759eccb66e6d495fb622eb7f4ac146ae674d829942ec18b7f5a35ddf029597"},
-    {file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246712af9fc761d6c13f4f065470982e175d902e77aa4218c9cb9fc9ff565a0c"},
-    {file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6b72dccc5864ea95c93e0a9c4e397708917fb450f96737b4a8395d009f90b868"},
-    {file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:93c78d42c14aa9a9e0866eacd5b48df40a50d0e2790ee377af7910d224afddcf"},
-    {file = "SQLAlchemy-2.0.9-cp39-cp39-win32.whl", hash = "sha256:f49c5d3c070a72ecb96df703966c9678dda0d4cb2e2736f88d15f5e1203b4159"},
-    {file = "SQLAlchemy-2.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:4c3020afb144572c7bfcba9d7cce57ad42bff6e6115dffcfe2d4ae6d444a214f"},
-    {file = "SQLAlchemy-2.0.9-py3-none-any.whl", hash = "sha256:e730603cae5747bc6d6dece98b45a57d647ed553c8d5ecef602697b1c1501cf2"},
-    {file = "SQLAlchemy-2.0.9.tar.gz", hash = "sha256:95719215e3ec7337b9f57c3c2eda0e6a7619be194a5166c07c1e599f6afc20fa"},
+    {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9deaae357edc2091a9ed5d25e9ee8bba98bcfae454b3911adeaf159c2e9ca9e3"},
+    {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bf0fd65b50a330261ec7fe3d091dfc1c577483c96a9fa1e4323e932961aa1b5"},
+    {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d90ccc15ba1baa345796a8fb1965223ca7ded2d235ccbef80a47b85cea2d71a"},
+    {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4e688f6784427e5f9479d1a13617f573de8f7d4aa713ba82813bcd16e259d1"},
+    {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:584f66e5e1979a7a00f4935015840be627e31ca29ad13f49a6e51e97a3fb8cae"},
+    {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c69ce70047b801d2aba3e5ff3cba32014558966109fecab0c39d16c18510f15"},
+    {file = "SQLAlchemy-2.0.19-cp310-cp310-win32.whl", hash = "sha256:96f0463573469579d32ad0c91929548d78314ef95c210a8115346271beeeaaa2"},
+    {file = "SQLAlchemy-2.0.19-cp310-cp310-win_amd64.whl", hash = "sha256:22bafb1da60c24514c141a7ff852b52f9f573fb933b1e6b5263f0daa28ce6db9"},
+    {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6894708eeb81f6d8193e996257223b6bb4041cb05a17cd5cf373ed836ef87a2"},
+    {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8f2afd1aafded7362b397581772c670f20ea84d0a780b93a1a1529da7c3d369"},
+    {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15afbf5aa76f2241184c1d3b61af1a72ba31ce4161013d7cb5c4c2fca04fd6e"},
+    {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc05b59142445a4efb9c1fd75c334b431d35c304b0e33f4fa0ff1ea4890f92e"},
+    {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5831138f0cc06b43edf5f99541c64adf0ab0d41f9a4471fd63b54ae18399e4de"},
+    {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3afa8a21a9046917b3a12ffe016ba7ebe7a55a6fc0c7d950beb303c735c3c3ad"},
+    {file = "SQLAlchemy-2.0.19-cp311-cp311-win32.whl", hash = "sha256:c896d4e6ab2eba2afa1d56be3d0b936c56d4666e789bfc59d6ae76e9fcf46145"},
+    {file = "SQLAlchemy-2.0.19-cp311-cp311-win_amd64.whl", hash = "sha256:024d2f67fb3ec697555e48caeb7147cfe2c08065a4f1a52d93c3d44fc8e6ad1c"},
+    {file = "SQLAlchemy-2.0.19-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:89bc2b374ebee1a02fd2eae6fd0570b5ad897ee514e0f84c5c137c942772aa0c"},
+    {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4d410a76c3762511ae075d50f379ae09551d92525aa5bb307f8343bf7c2c12"},
+    {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f469f15068cd8351826df4080ffe4cc6377c5bf7d29b5a07b0e717dddb4c7ea2"},
+    {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cda283700c984e699e8ef0fcc5c61f00c9d14b6f65a4f2767c97242513fcdd84"},
+    {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:43699eb3f80920cc39a380c159ae21c8a8924fe071bccb68fc509e099420b148"},
+    {file = "SQLAlchemy-2.0.19-cp37-cp37m-win32.whl", hash = "sha256:61ada5831db36d897e28eb95f0f81814525e0d7927fb51145526c4e63174920b"},
+    {file = "SQLAlchemy-2.0.19-cp37-cp37m-win_amd64.whl", hash = "sha256:57d100a421d9ab4874f51285c059003292433c648df6abe6c9c904e5bd5b0828"},
+    {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16a310f5bc75a5b2ce7cb656d0e76eb13440b8354f927ff15cbaddd2523ee2d1"},
+    {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf7b5e3856cbf1876da4e9d9715546fa26b6e0ba1a682d5ed2fc3ca4c7c3ec5b"},
+    {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e7b69d9ced4b53310a87117824b23c509c6fc1f692aa7272d47561347e133b6"},
+    {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9eb4575bfa5afc4b066528302bf12083da3175f71b64a43a7c0badda2be365"},
+    {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6b54d1ad7a162857bb7c8ef689049c7cd9eae2f38864fc096d62ae10bc100c7d"},
+    {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5d6afc41ca0ecf373366fd8e10aee2797128d3ae45eb8467b19da4899bcd1ee0"},
+    {file = "SQLAlchemy-2.0.19-cp38-cp38-win32.whl", hash = "sha256:430614f18443b58ceb9dedec323ecddc0abb2b34e79d03503b5a7579cd73a531"},
+    {file = "SQLAlchemy-2.0.19-cp38-cp38-win_amd64.whl", hash = "sha256:eb60699de43ba1a1f77363f563bb2c652f7748127ba3a774f7cf2c7804aa0d3d"},
+    {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a752b7a9aceb0ba173955d4f780c64ee15a1a991f1c52d307d6215c6c73b3a4c"},
+    {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7351c05db355da112e056a7b731253cbeffab9dfdb3be1e895368513c7d70106"},
+    {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa51ce4aea583b0c6b426f4b0563d3535c1c75986c4373a0987d84d22376585b"},
+    {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae7473a67cd82a41decfea58c0eac581209a0aa30f8bc9190926fbf628bb17f7"},
+    {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:851a37898a8a39783aab603c7348eb5b20d83c76a14766a43f56e6ad422d1ec8"},
+    {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539010665c90e60c4a1650afe4ab49ca100c74e6aef882466f1de6471d414be7"},
+    {file = "SQLAlchemy-2.0.19-cp39-cp39-win32.whl", hash = "sha256:f82c310ddf97b04e1392c33cf9a70909e0ae10a7e2ddc1d64495e3abdc5d19fb"},
+    {file = "SQLAlchemy-2.0.19-cp39-cp39-win_amd64.whl", hash = "sha256:8e712cfd2e07b801bc6b60fdf64853bc2bd0af33ca8fa46166a23fe11ce0dbb0"},
+    {file = "SQLAlchemy-2.0.19-py3-none-any.whl", hash = "sha256:314145c1389b021a9ad5aa3a18bac6f5d939f9087d7fc5443be28cba19d2c972"},
+    {file = "SQLAlchemy-2.0.19.tar.gz", hash = "sha256:77a14fa20264af73ddcdb1e2b9c5a829b8cc6b8304d0f093271980e36c200a3f"},
 ]
 
 [package.dependencies]
@@ -1062,6 +1058,7 @@ postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
 postgresql-psycopg = ["psycopg (>=3.0.7)"]
 postgresql-psycopg2binary = ["psycopg2-binary"]
 postgresql-psycopg2cffi = ["psycopg2cffi"]
+postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
 pymysql = ["pymysql"]
 sqlcipher = ["sqlcipher3-binary"]
 
@@ -1079,26 +1076,26 @@ files = [
 
 [[package]]
 name = "tomlkit"
-version = "0.11.7"
+version = "0.12.1"
 description = "Style preserving TOML library"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"},
-    {file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"},
+    {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"},
+    {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"},
 ]
 
 [[package]]
 name = "typing-extensions"
-version = "4.5.0"
+version = "4.7.1"
 description = "Backported and Experimental Type Hints for Python 3.7+"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
-    {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
+    {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
+    {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
 ]
 
 [[package]]
@@ -1115,14 +1112,14 @@ files = [
 
 [[package]]
 name = "werkzeug"
-version = "2.3.3"
+version = "2.3.6"
 description = "The comprehensive WSGI web application library."
 category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "Werkzeug-2.3.3-py3-none-any.whl", hash = "sha256:4866679a0722de00796a74086238bb3b98d90f423f05de039abb09315487254a"},
-    {file = "Werkzeug-2.3.3.tar.gz", hash = "sha256:a987caf1092edc7523edb139edb20c70571c4a8d5eed02e0b547b4739174d091"},
+    {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"},
+    {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"},
 ]
 
 [package.dependencies]
@@ -1218,21 +1215,21 @@ files = [
 
 [[package]]
 name = "zipp"
-version = "3.15.0"
+version = "3.16.2"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
-    {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
+    {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"},
+    {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"},
 ]
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
 
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "15ba5eebb3e5385a9e2ab48b5d6156bd59df6b7a1d431c54d95bba5a8ec53004"
+content-hash = "96ec0d1f7b512afb05455262fa2de8c4f862bf68fdae513f8552dc30c6e5ab49"
diff --git a/pyproject.toml b/pyproject.toml
index f32f3b0..3752e8a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "OnlyLegs"
-version = "0.1.2"
+version = "0.1.5"
 repository = "https://github.com/Fluffy-Bean/onlylegs"
 license = "MIT"
 readme = "README.md"
@@ -13,7 +13,7 @@ Flask = "^2.3.2"
 Flask-Sqlalchemy = "^3.0.3"
 Flask-Migrate = "^4.0.4"
 Flask-Compress = "^1.13"
-Flask-Caching = "^2.0.2" 
+Flask-Caching = "1.10.1" 
 Flask-Assets = "^2.0"
 Flask-Login = "^0.6.2"
 python-dotenv = "^0.21.0"
@@ -27,6 +27,7 @@ jsmin = "^3.0.1"
 cssmin = "^0.2.0"
 pylint = "^2.16.3"
 black = "^23.3.0"
+cachetools = "^5.3.0"
 
 [build-system]
 requires = ["poetry-core"]
diff --git a/run.py b/run.py
index 8653117..a10fa90 100644
--- a/run.py
+++ b/run.py
@@ -25,9 +25,9 @@ Configuration()
 
 
 if DEBUG:
-    from onlylegs import create_app
+    from onlylegs.app import app
 
-    create_app().run(host=ADDRESS, port=PORT, debug=True, threaded=True)
+    app.run(host=ADDRESS, port=PORT, debug=True, threaded=True)
 else:
     from setup.runner import OnlyLegs  # pylint: disable=C0412
     import sys
diff --git a/setup/runner.py b/setup/runner.py
index 76a8a32..94cd827 100644
--- a/setup/runner.py
+++ b/setup/runner.py
@@ -32,4 +32,4 @@ class OnlyLegs(Application):
         return "OnlyLegs"
 
     def load(self):
-        return util.import_app("onlylegs:create_app()")
+        return util.import_app("onlylegs.app:app")