diff --git a/.gitignore b/.gitignore
index 650aa90..9a7743d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,6 @@
 /.idea
-/conf/app.ini
\ No newline at end of file
+/conf/app.ini
+/node_modules
+/public/assets/css/*
+/public/assets/js/*
+package-lock.json
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0c6e5cf
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+    "scripts": {
+        "build": "webpack --config webpack.config.js --mode=production",
+        "dev": "webpack --config webpack.config.js --mode=development",
+        "watch": "webpack --config webpack.config.js --mode=development --watch"
+    },
+    "devDependencies": {
+        "webpack": "^5.99.5",
+        "webpack-cli": "^6.0.1",
+        "autoprefixer": "^10.4.21",
+        "css-loader": "^7.1.2",
+        "css-minimizer-webpack-plugin": "^7.0.2",
+        "ignore-emit-webpack-plugin": "^2.0.6",
+        "mini-css-extract-plugin": "^2.9.2",
+        "postcss": "^8.5.3",
+        "postcss-loader": "^8.1.1",
+        "sass": "^1.86.3",
+        "sass-loader": "^16.0.5",
+        "terser-webpack-plugin": "^5.3.14",
+        "ts-loader": "^9.5.2",
+        "typescript": "^5.8.3"
+    }
+}
\ No newline at end of file
diff --git a/src/js/loom.ts b/src/js/loom.ts
new file mode 100644
index 0000000..2a03d6e
--- /dev/null
+++ b/src/js/loom.ts
@@ -0,0 +1 @@
+console.log('Welcome to Loom Forge!');
\ No newline at end of file
diff --git a/src/scss/_config.scss b/src/scss/_config.scss
new file mode 100644
index 0000000..0265499
--- /dev/null
+++ b/src/scss/_config.scss
@@ -0,0 +1,4 @@
+$spacingLevels: 8;
+$fontSizeIncrement: 0.125rem;
+$fontSizeBase: 1rem;
+$fontLevels: 20;
\ No newline at end of file
diff --git a/src/scss/core/_display.scss b/src/scss/core/_display.scss
new file mode 100644
index 0000000..7595ac2
--- /dev/null
+++ b/src/scss/core/_display.scss
@@ -0,0 +1,12 @@
+@mixin generateClasses {
+    .loom- {
+        &display- {
+            &-block {
+                display: block;
+            }
+            &-inline-block {
+                display: inline-block;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/scss/core/_spacing.scss b/src/scss/core/_spacing.scss
new file mode 100644
index 0000000..ae32a73
--- /dev/null
+++ b/src/scss/core/_spacing.scss
@@ -0,0 +1,64 @@
+@mixin marginClasses($i, $calc) {
+    &-m {
+        &-#{$i} {
+            margin: $calc;
+        }
+        &x-#{$i} {
+            margin-left: $calc;
+            margin-right: $calc;
+        }
+        &y-#{$i} {
+            margin-top: $calc;
+            margin-bottom: $calc;
+        }
+        &t-#{$i} {
+            margin-top: $calc;
+        }
+        &b-#{$i} {
+            margin-bottom: $calc;
+        }
+        &l-#{$i} {
+            margin-left: $calc;
+        }
+        &r-#{$i} {
+            margin-right: $calc;
+        }
+    }
+}
+
+@mixin paddingClasses($i, $calc) {
+    &-p {
+        &-#{$i} {
+            padding: $calc;
+        }
+        &x-#{$i} {
+            padding-left: $calc;
+            padding-right: $calc;
+        }
+        &y-#{$i} {
+            padding-top: $calc;
+            padding-bottom: $calc;
+        }
+        &t-#{$i} {
+            padding-top: $calc;
+        }
+        &b-#{$i} {
+            padding-bottom: $calc;
+        }
+        &l-#{$i} {
+            padding-left: $calc;
+        }
+        &r-#{$i} {
+            padding-right: $calc;
+        }
+    }
+}
+@mixin generateClasses($spacingLevels) {
+    @for $i from 1 through $spacingLevels {
+        $calc: calc(0.25rem * $i);
+        .loom {
+            @include marginClasses($i, $calc);
+            @include paddingClasses($i, $calc);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/scss/loom.scss b/src/scss/loom.scss
new file mode 100644
index 0000000..7bd8620
--- /dev/null
+++ b/src/scss/loom.scss
@@ -0,0 +1,8 @@
+@use 'config';
+@use 'core/display';
+@use 'core/spacing';
+@use 'text/fontSize';
+
+@include display.generateClasses();
+@include spacing.generateClasses(config.$spacingLevels);
+@include fontSize.generateCleanClasses(config.$fontSizeBase, config.$fontSizeIncrement, config.$fontLevels);
\ No newline at end of file
diff --git a/src/scss/text/_fontSize.scss b/src/scss/text/_fontSize.scss
new file mode 100644
index 0000000..c255fa1
--- /dev/null
+++ b/src/scss/text/_fontSize.scss
@@ -0,0 +1,33 @@
+@mixin generateNumericClasses($fontSizeStart, $fontSizeIncrement, $levels) {
+    @for $i from 1 through $levels {
+        .loom- {
+            &-text-size-#{$i} {
+                font-size: calc($fontSizeStart + ($fontSizeIncrement * ($i - 1)));
+            }
+        }
+    }
+}
+
+@mixin generateCleanClasses($fontSizeBase, $fontSizeIncrement, $fontLevels) {
+    .loom- {
+        &text-size- {
+            &xs {
+                font-size: calc($fontSizeBase - ($fontSizeIncrement * 2));
+            }
+            &sm {
+                font-size: calc($fontSizeBase - $fontSizeIncrement);
+            }
+            &base {
+                font-size: $fontSizeBase;
+            }
+            &l {
+                font-size: calc($fontSizeBase + $fontSizeIncrement);
+            }
+            @for $i from 2 through $fontLevels {
+                &#{$i}l {
+                    font-size: calc($fontSizeBase + ($fontSizeIncrement * $i));
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl
new file mode 100644
index 0000000..3261470
--- /dev/null
+++ b/templates/base/footer.tmpl
@@ -0,0 +1,21 @@
+{{if false}}
+	{{/* to make html structure "likely" complete to prevent IDE warnings */}}
+<html>
+<body>
+	<div>
+{{end}}
+
+	{{template "custom/body_inner_post" .}}
+
+	</div>
+
+	{{template "custom/body_outer_post" .}}
+
+	{{template "base/footer_content" .}}
+
+	<script src="{{AssetUrlPrefix}}/js/index.js?v={{AssetVersion}}" onerror="alert('{{ctx.Locale.Tr "alert.asset_load_failed"}}'.replace('{path}', this.src))"></script>
+	<script src="/assets/js/loomScripts.js"></script>
+
+	{{template "custom/footer" .}}
+</body>
+</html>
diff --git a/templates/base/footer_content.tmpl b/templates/base/footer_content.tmpl
new file mode 100644
index 0000000..47cd5b9
--- /dev/null
+++ b/templates/base/footer_content.tmpl
@@ -0,0 +1,30 @@
+<footer class="page-footer" role="group" aria-label="{{ctx.Locale.Tr "aria.footer"}}">
+	<div class="left-links" role="contentinfo" aria-label="{{ctx.Locale.Tr "aria.footer.software"}}">
+		{{if ShowFooterPoweredBy}}
+			<a target="_blank" rel="noopener noreferrer" href="https://forgejo.org">{{ctx.Locale.Tr "powered_by" "Forgejo"}}</a>
+		{{end}}
+		{{if (or .ShowFooterVersion .PageIsAdmin)}}
+			{{ctx.Locale.Tr "version"}}:
+			{{if .IsAdmin}}
+				<a href="{{AppSubUrl}}/admin/config">{{AppVer}}</a>
+			{{else}}
+				{{AppVerNoMetadata}}
+			{{end}}
+		{{end}}
+		{{if and .TemplateLoadTimes ShowFooterTemplateLoadTime}}
+			{{ctx.Locale.Tr "page"}}: <strong>{{LoadTimes .PageStartTime}}</strong>
+			{{ctx.Locale.Tr "template"}}{{if .TemplateName}} {{.TemplateName}}{{end}}: <strong>{{call .TemplateLoadTimes}}</strong>
+		{{end}}
+	</div>
+	<div class="right-links" role="group" aria-label="{{ctx.Locale.Tr "aria.footer.links"}}">
+		<div class="ui dropdown upward language">
+			<span class="flex-text-inline">{{svg "octicon-globe" 14}} {{ctx.Locale.LangName}}</span>
+			<div class="menu language-menu">
+				{{range .AllLangs}}
+					<a lang="{{.Lang}}" data-url="{{AppSubUrl}}/?lang={{.Lang}}" class="item {{if eq ctx.Locale.Lang .Lang}}active selected{{end}}">{{.Name}}</a>
+				{{end}}
+			</div>
+		</div>
+		{{template "custom/extra_links_footer" .}}
+	</div>
+</footer>
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
new file mode 100644
index 0000000..c029407
--- /dev/null
+++ b/templates/base/head.tmpl
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html lang="{{ctx.Locale.Lang}}" data-theme="{{ThemeName .SignedUser}}">
+<head>
+	<meta name="viewport" content="width=device-width, initial-scale=1">
+	{{/* Display `- .Repository.FullName` only if `.Title` does not already start with that. */}}
+	<title>{{if .Title}}{{.Title}} - {{end}}{{if and (.Repository.Name) (not (StringUtils.HasPrefix .Title .Repository.FullName))}}{{.Repository.FullName}} - {{end}}{{AppDisplayName}}</title>
+	{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
+	<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">
+	<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
+	<meta name="keywords" content="{{MetaKeywords}}">
+	<meta name="referrer" content="no-referrer">
+{{if .GoGetImport}}
+	<meta name="go-import" content="{{.GoGetImport}} git {{.RepoCloneLink.HTTPS}}">
+	<meta name="go-source" content="{{.GoGetImport}} _ {{.GoDocDirectory}} {{.GoDocFile}}">
+{{end}}
+{{if and .EnableFeed .FeedURL}}
+	<link rel="alternate" type="application/atom+xml" title="" href="{{.FeedURL}}.atom">
+	<link rel="alternate" type="application/rss+xml" title="" href="{{.FeedURL}}.rss">
+{{end}}
+	<link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml">
+	<link rel="alternate icon" href="{{AssetUrlPrefix}}/img/favicon.png" type="image/png">
+	{{template "base/head_script" .}}
+	{{template "shared/user/mention_highlight" .}}
+	{{template "base/head_opengraph" .}}
+	{{template "base/head_style" .}}
+	<link rel="stylesheet" type="text/css" href="/assets/css/loomStyles.css">
+	{{template "custom/header" .}}
+</head>
+<body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-ext="morph" hx-push-url="false">
+	{{template "custom/body_outer_pre" .}}
+
+	<div class="full height">
+		<noscript>{{ctx.Locale.Tr "enable_javascript"}}</noscript>
+
+		{{template "custom/body_inner_pre" .}}
+
+		{{if not .PageIsInstall}}
+			{{template "base/head_navbar" .}}
+		{{end}}
+
+{{if false}}
+	{{/* to make html structure "likely" complete to prevent IDE warnings */}}
+	</div>
+</body>
+</html>
+{{end}}
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
new file mode 100644
index 0000000..0bd25ea
--- /dev/null
+++ b/templates/base/head_navbar.tmpl
@@ -0,0 +1,203 @@
+{{$notificationUnreadCount := 0}}
+{{if and .IsSigned .NotificationUnreadCount}}
+	{{$notificationUnreadCount = call .NotificationUnreadCount}}
+{{end}}
+
+<nav id="navbar" aria-label="{{ctx.Locale.Tr "aria.navbar"}}">
+	<div class="navbar-left ui secondary menu">
+		<!-- the logo -->
+		<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home"}}{{end}}">
+			<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
+		</a>
+
+		<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
+		<div class="ui secondary menu item navbar-mobile-right only-mobile">
+			{{if .IsSigned}}
+			<a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
+				<div class="tw-relative">
+					{{svg "octicon-bell"}}
+					<span class="notification_count{{if not $notificationUnreadCount}} tw-hidden{{end}}">{{$notificationUnreadCount}}</span>
+				</div>
+			</a>
+			{{end}}
+			<button class="item tw-w-auto ui icon mini button tw-p-2 tw-m-0" id="navbar-expand-toggle" aria-label="{{ctx.Locale.Tr "toggle_menu"}}">{{svg "octicon-three-bars"}}</button>
+		</div>
+
+		<!-- navbar links non-mobile -->
+		{{if and .IsSigned .MustChangePassword}}
+			{{/* No links */}}
+		{{else if .IsSigned}}
+			{{if not .UnitIssuesGlobalDisabled}}
+				<a class="item{{if .PageIsIssues}} active{{end}}" href="{{AppSubUrl}}/issues">{{ctx.Locale.Tr "issues"}}</a>
+			{{end}}
+			{{if not .UnitPullsGlobalDisabled}}
+				<a class="item{{if .PageIsPulls}} active{{end}}" href="{{AppSubUrl}}/pulls">{{ctx.Locale.Tr "pull_requests"}}</a>
+			{{end}}
+			{{if not (and .UnitIssuesGlobalDisabled .UnitPullsGlobalDisabled)}}
+				{{if .ShowMilestonesDashboardPage}}
+					<a class="item{{if .PageIsMilestonesDashboard}} active{{end}}" href="{{AppSubUrl}}/milestones">{{ctx.Locale.Tr "milestones"}}</a>
+				{{end}}
+			{{end}}
+			<a class="item{{if .PageIsExplore}} active{{end}}" href="{{AppSubUrl}}/explore/repos">{{ctx.Locale.Tr "explore"}}</a>
+		{{else if .IsLandingPageOrganizations}}
+			<a class="item{{if .PageIsExplore}} active{{end}}" href="{{AppSubUrl}}/explore/organizations">{{ctx.Locale.Tr "explore"}}</a>
+		{{else}}
+			<a class="item{{if .PageIsExplore}} active{{end}}" href="{{AppSubUrl}}/explore/repos">{{ctx.Locale.Tr "explore"}}</a>
+		{{end}}
+
+		{{template "custom/extra_links" .}}
+	</div>
+
+	<!-- the full dropdown menus -->
+	<div class="navbar-right ui secondary menu">
+		{{if and .IsSigned .MustChangePassword}}
+			<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
+				<span class="text tw-flex tw-items-center">
+					{{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}}
+					<span class="only-mobile tw-ml-2">{{.SignedUser.Name}}</span>
+					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
+				</span>
+				<div class="menu user-menu">
+					<div class="ui header">
+						{{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong>
+					</div>
+
+					<div class="divider"></div>
+					<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
+						{{svg "octicon-sign-out"}}
+						{{ctx.Locale.Tr "sign_out"}}
+					</a>
+				</div><!-- end content avatar menu -->
+			</div><!-- end dropdown avatar menu -->
+		{{else if .IsSigned}}
+			{{if EnableTimetracking}}
+			<a class="active-stopwatch-trigger item tw-mx-0{{if not .ActiveStopwatch}} tw-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}">
+				<div class="tw-relative">
+					{{svg "octicon-stopwatch"}}
+					<span class="header-stopwatch-dot"></span>
+				</div>
+				<span class="only-mobile tw-ml-2">{{ctx.Locale.Tr "active_stopwatch"}}</span>
+			</a>
+			<div class="active-stopwatch-popup item tippy-target tw-p-2">
+				<div class="tw-flex tw-items-center">
+					<a class="stopwatch-link tw-flex tw-items-center" href="{{.ActiveStopwatch.IssueLink}}">
+						{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
+						<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
+						<span class="ui primary label stopwatch-time tw-my-0 tw-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
+							{{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}}
+						</span>
+					</a>
+					<form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
+						{{.CsrfTokenHtml}}
+						<button
+							type="submit"
+							class="ui button mini compact basic icon"
+							data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
+						>{{svg "octicon-square-fill"}}</button>
+					</form>
+					<form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
+						{{.CsrfTokenHtml}}
+						<button
+							type="submit"
+							class="ui button mini compact basic icon"
+							data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
+						>{{svg "octicon-trash"}}</button>
+					</form>
+				</div>
+			</div>
+			{{end}}
+
+			<a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
+				<div class="tw-relative">
+					{{svg "octicon-bell"}}
+					<span class="notification_count{{if not $notificationUnreadCount}} tw-hidden{{end}}">{{$notificationUnreadCount}}</span>
+				</div>
+			</a>
+
+			<div class="ui dropdown jump item tw-mx-0 tw-pr-2" data-tooltip-content="{{ctx.Locale.Tr "create_new"}}">
+				<span class="text">
+					{{svg "octicon-plus"}}
+					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
+					<span class="only-mobile">{{ctx.Locale.Tr "create_new"}}</span>
+				</span>
+				<div class="menu">
+					<a class="item" href="{{AppSubUrl}}/repo/create">
+						{{svg "octicon-plus"}} {{ctx.Locale.Tr "new_repo.link"}}
+					</a>
+					{{if not .DisableMigrations}}
+						<a class="item" href="{{AppSubUrl}}/repo/migrate">
+							{{svg "octicon-repo-push"}} {{ctx.Locale.Tr "new_migrate.link"}}
+						</a>
+					{{end}}
+					{{if .SignedUser.CanCreateOrganization}}
+					<a class="item" href="{{AppSubUrl}}/org/create">
+						{{svg "octicon-organization"}} {{ctx.Locale.Tr "new_org.link"}}
+					</a>
+					{{end}}
+				</div><!-- end content create new menu -->
+			</div><!-- end dropdown menu create new -->
+
+			<div class="ui dropdown jump item tw-mx-0 tw-pr-2" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
+				<span class="text tw-flex tw-items-center">
+					{{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}}
+					<span class="only-mobile tw-ml-2">{{.SignedUser.Name}}</span>
+					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
+				</span>
+				<div class="menu user-menu">
+					<div class="ui header">
+						{{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong>
+					</div>
+
+					<div class="divider"></div>
+					<a class="item" href="{{.SignedUser.HomeLink}}">
+						{{svg "octicon-person"}}
+						{{ctx.Locale.Tr "your_profile"}}
+					</a>
+					{{if not .DisableStars}}
+						<a class="item" href="{{.SignedUser.HomeLink}}?tab=stars">
+							{{svg "octicon-star"}}
+							{{ctx.Locale.Tr "your_starred"}}
+						</a>
+					{{end}}
+					<a class="item" href="{{AppSubUrl}}/notifications/subscriptions">
+						{{svg "octicon-bell"}}
+						{{ctx.Locale.Tr "notification.subscriptions"}}
+					</a>
+					<a class="{{if .PageIsUserSettings}}active {{end}}item" href="{{AppSubUrl}}/user/settings">
+						{{svg "octicon-tools"}}
+						{{ctx.Locale.Tr "your_settings"}}
+					</a>
+					<a class="item" target="_blank" rel="noopener noreferrer" href="https://forgejo.org/docs/latest/">
+						{{svg "octicon-question"}}
+						{{ctx.Locale.Tr "help"}}
+					</a>
+					{{if .IsAdmin}}
+						<div class="divider"></div>
+
+						<a class="{{if .PageIsAdmin}}active {{end}}item" href="{{AppSubUrl}}/admin">
+							{{svg "octicon-server"}}
+							{{ctx.Locale.Tr "admin_panel"}}
+						</a>
+					{{end}}
+
+					<div class="divider"></div>
+					<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
+						{{svg "octicon-sign-out"}}
+						{{ctx.Locale.Tr "sign_out"}}
+					</a>
+				</div><!-- end content avatar menu -->
+			</div><!-- end dropdown avatar menu -->
+		{{else}}
+			{{if .ShowRegistrationButton}}
+				<a class="item{{if .PageIsSignUp}} active{{end}}" href="{{AppSubUrl}}/user/sign_up">
+					{{svg "octicon-person" 16 "tw-mr-1"}}
+					<span>{{ctx.Locale.Tr "register"}}</span>
+				</a>
+			{{end}}
+			<a class="item{{if .PageIsSignIn}} active{{end}}" rel="nofollow" href="{{AppSubUrl}}/user/login{{if not .PageIsSignIn}}?redirect_to={{.CurrentURL}}{{end}}">
+				{{svg "octicon-sign-in" 16 "tw-mr-1"}}
+				<span>{{ctx.Locale.Tr "sign_in"}}</span>
+			</a>
+		{{end}}
+	</div><!-- end full right menu -->
+</nav>
diff --git a/templates/home.tmpl b/templates/home.tmpl
index 9cc9153..a6af086 100644
--- a/templates/home.tmpl
+++ b/templates/home.tmpl
@@ -1,8 +1,8 @@
 {{template "base/head" .}}
 <div role="main" aria-label="Welcome" class="page-content home">
-	<div class="tw-mb-8 tw-px-8 center">
-		<div class="hero">
-			<h1 class="ui icon header title">
+	<div class="center">
+		<div>
+			<h1 class="tw-font-bold loom-text-size-16l">
 				Loom Forge
 			</h1>
 		</div>
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..6d0fe5f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,13 @@
+{
+    "compilerOptions": {
+        "outDir": "./public/assets/js",
+        "noImplicitAny": true,
+        "module": "commonjs",
+        "target": "es6",
+        "allowJs": true,
+        "moduleResolution": "node",
+        "esModuleInterop": true,
+        "skipLibCheck": true
+    },
+    "include": ["./src/js/loom.ts"]
+}
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..9600fb0
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,77 @@
+const path = require('path');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const IgnoreEmitPlugin = require('ignore-emit-webpack-plugin');
+const TerserPlugin = require('terser-webpack-plugin');
+const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
+
+const entries = {
+    loomScripts: './src/js/loom.ts',
+    loomStyles: './src/scss/loom.scss',
+}
+const ignoreFiles = Object.keys(entries).reduce((acc, key) => {
+    if (entries[key].endsWith('.scss')) {
+        acc.push(`${key}.js`);
+    }
+
+    return acc;
+}, []);
+
+module.exports = (env, options) => {
+    const isProduction = options.mode === 'production';
+
+    return {
+        entry: entries,
+        module: {
+            rules: [
+                {
+                    test: /\.ts?$/,
+                    use: 'ts-loader',
+                    exclude: /node_modules/
+                },
+                {
+                    test: /\.s[ac]ss$/i,
+                    use: [
+                        MiniCssExtractPlugin.loader,
+                        'css-loader',
+                        {
+                            loader: 'postcss-loader',
+                            options: {
+                                postcssOptions: {
+                                    plugins: [
+                                        require('autoprefixer')
+                                    ]
+                                }
+                            }
+                        },
+                        'sass-loader'
+                    ],
+                    exclude: /node_modules/
+                },
+            ],
+        },
+        plugins: [
+            new MiniCssExtractPlugin({
+                filename: 'css/[name].css',
+            }),
+            new IgnoreEmitPlugin(ignoreFiles)
+        ],
+        optimization: isProduction ? {
+            minimize: true,
+            minimizer: [
+                new TerserPlugin(),
+                new CssMinimizerPlugin(),
+            ]
+        } : {},
+        resolve: {
+            extensions: ['.ts', '.js', '.css', '.scss']
+        },
+        watchOptions: {
+            poll: true,
+            ignored: /node_modules/
+        },
+        output: {
+            filename: 'js/[name].js',
+            path: path.resolve(__dirname, 'public/assets/')
+        }
+    };
+}
\ No newline at end of file