diff --git a/templates/_layout.html b/templates/_layout.html
index fb75ad3..35cbd24 100644
--- a/templates/_layout.html
+++ b/templates/_layout.html
@@ -32,6 +32,8 @@
+
+
diff --git a/website/dynamic-rotation.js b/website/dynamic-rotation.js
new file mode 100644
index 0000000..12a373e
--- /dev/null
+++ b/website/dynamic-rotation.js
@@ -0,0 +1,55 @@
+(function() {
+ function updatePostRotations() {
+ const posts = document.querySelectorAll('.post');
+
+ posts.forEach(post => {
+ const height = post.offsetHeight;
+
+ // Calculate rotation: 3deg for 200px posts, 0.5deg for 500px+ posts
+ const minRotation = 0.5;
+ const maxRotation = 2.5;
+ const minHeight = 200;
+ const maxHeight = 700;
+
+ // Linear interpolation between min and max rotation based on height
+ let rotation = maxRotation - ((height - minHeight) / (maxHeight - minHeight)) * (maxRotation - minRotation);
+
+ // Clamp between min and max
+ rotation = Math.max(minRotation, Math.min(maxRotation, rotation));
+
+ // Apply alternating direction
+ const isOdd = Array.from(posts).indexOf(post) % 2 === 0;
+ const finalRotation = isOdd ? -rotation : rotation;
+
+ // Set CSS custom property
+ post.style.setProperty('--dynamic-rotation', `${finalRotation}deg`);
+ post.style.transform = `rotate(var(--dynamic-rotation, ${finalRotation}deg))`;
+ });
+ }
+
+ // Update rotations when DOM is ready
+ function init() {
+ // Wait for images to load to get accurate heights
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ setTimeout(updatePostRotations, 100);
+ });
+ } else {
+ setTimeout(updatePostRotations, 100);
+ }
+
+ // Update after window load for images
+ window.addEventListener('load', () => {
+ setTimeout(updatePostRotations, 200);
+ });
+
+ // Update on resize with debouncing
+ let resizeTimeout;
+ window.addEventListener('resize', () => {
+ clearTimeout(resizeTimeout);
+ resizeTimeout = setTimeout(updatePostRotations, 300);
+ });
+ }
+
+ init();
+})();
diff --git a/website/index.html b/website/index.html
index a9473f4..9d212bd 100644
--- a/website/index.html
+++ b/website/index.html
@@ -32,6 +32,8 @@
+
+
Litruv - Dev/Tech Artist
diff --git a/website/main.js b/website/main.js
index 2abcb6f..6189164 100644
--- a/website/main.js
+++ b/website/main.js
@@ -1 +1,162 @@
-function formatDate(t){return new Date(t).toLocaleDateString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}function getRandomRotation(){return 3*Math.random()-1.5}function createPost(t){const e=document.createElement("div");e.className="post",e.style.transform=`rotate(${getRandomRotation()}deg)`;const o=document.createElement("a");o.href=t.url,o.target="_blank",o.style.textDecoration="none";const n=t.embed.length>0?t.embed.map((t=>`\n \n
\n \n `)).join(""):"",a=t.text.replace(/\n/g,"
");return e.innerHTML=`\n \n ${a}
\n ${n}\n \n \n \n
\n `,o.href=t.url,o.appendChild(e),o}function filterOriginalPosts(t){return t.filter((t=>{const e="app.bsky.feed.defs#reasonRepost"===t?.reason?.$type,o=t?.post?.record?.reply;return!e&&!o}))}async function fetchPosts(){try{const t=await fetch("https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=lit.mates.dev&limit=20&filter=posts_no_replies");if(!t.ok)throw new Error("Failed to fetch posts");const e=await t.json(),o=filterOriginalPosts(e.feed).map((t=>({author:t.post.author.displayName,handle:t.post.author.handle,avatar:t.post.author.avatar,createdAt:t.post.record.createdAt,text:t.post.record.text,likes:t.post.likeCount||0,reposts:t.post.repostCount||0,url:`https://bsky.app/profile/${t.post.author.handle}/post/${t.post.uri.split("/").pop()}`,embed:t.post.embed?.images?.map((t=>({url:t.thumb,alt:t.alt||""})))||[]}))),n=document.getElementById("posts");n.innerHTML="",o.forEach((t=>{n.appendChild(createPost(t))}))}catch(t){console.error("Error fetching posts:",t)}}fetchPosts();
\ No newline at end of file
+function formatDate(t) {
+ return new Date(t).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit"
+ });
+}
+
+function getRandomRotation() {
+ return 3 * Math.random() - 1.5;
+}
+
+function createPost(t) {
+ const post = document.createElement("div");
+ post.className = "post";
+ post.style.transform = `rotate(${getRandomRotation()}deg)`;
+
+ const link = document.createElement("a");
+ link.href = t.url;
+ link.target = "_blank";
+ link.style.textDecoration = "none";
+
+ const embedHtml = t.embed.length > 0
+ ? t.embed.map(img =>
+ `
+
+
+
+ `
+ ).join("")
+ : "";
+
+ const textHtml = t.text.replace(/\n/g, "
");
+
+ post.innerHTML = `
+
+ ${textHtml}
+ ${embedHtml}
+
+
+
+
+ `;
+
+ link.href = t.url;
+ link.appendChild(post);
+ return link;
+}
+
+function filterOriginalPosts(feed) {
+ return feed.filter(item => {
+ const isRepost = item?.reason?.$type === "app.bsky.feed.defs#reasonRepost";
+ const isReply = item?.post?.record?.reply;
+ return !isRepost && !isReply;
+ });
+}
+
+let postElementsCache = null;
+
+function createColumns(num) {
+ const postsContainer = document.getElementById("posts");
+ postsContainer.innerHTML = "";
+ const columns = [];
+ for (let i = 0; i < num; i++) {
+ const col = document.createElement("div");
+ col.className = "masonry-col";
+ postsContainer.appendChild(col);
+ columns.push(col);
+ }
+ return columns;
+}
+
+function getNumColumns() {
+ if (window.innerWidth >= 1200) return 3;
+ if (window.innerWidth >= 800) return 2;
+ return 1;
+}
+
+function getShortestColumn(columns) {
+ let minHeight = columns[0].offsetHeight;
+ let minCol = columns[0];
+ for (const col of columns) {
+ if (col.offsetHeight < minHeight) {
+ minHeight = col.offsetHeight;
+ minCol = col;
+ }
+ }
+ return minCol;
+}
+
+function layoutPosts(postElements) {
+ const numCols = getNumColumns();
+ const columns = createColumns(numCols);
+ postElements.forEach(postEl => {
+ const col = getShortestColumn(columns);
+ col.appendChild(postEl);
+ });
+}
+
+async function fetchPosts() {
+ try {
+ const res = await fetch(
+ "https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=lit.mates.dev&limit=20&filter=posts_no_replies"
+ );
+ if (!res.ok) throw new Error("Failed to fetch posts");
+ const data = await res.json();
+
+ const posts = filterOriginalPosts(data.feed).map(item => ({
+ author: item.post.author.displayName,
+ handle: item.post.author.handle,
+ avatar: item.post.author.avatar,
+ createdAt: item.post.record.createdAt,
+ text: item.post.record.text,
+ likes: item.post.likeCount || 0,
+ reposts: item.post.repostCount || 0,
+ url: `https://bsky.app/profile/${item.post.author.handle}/post/${item.post.uri.split("/").pop()}`,
+ embed: item.post.embed?.images?.map(img => ({
+ url: img.thumb,
+ alt: img.alt || ""
+ })) || []
+ }));
+
+ // Only create post elements once
+ postElementsCache = posts.map(post => createPost(post));
+ // Preload images
+ await Promise.all(postElementsCache.map(el => {
+ const imgs = el.querySelectorAll("img");
+ if (!imgs.length) return Promise.resolve();
+ return Promise.all(Array.from(imgs).map(img => {
+ if (img.complete) return Promise.resolve();
+ return new Promise(res => {
+ img.onload = img.onerror = res;
+ });
+ }));
+ }));
+
+ layoutPosts(postElementsCache);
+
+ } catch (err) {
+ console.error("Error fetching posts:", err);
+ }
+}
+
+// Only re-layout on resize, do not re-fetch or re-create posts
+window.addEventListener("resize", () => {
+ if (postElementsCache) {
+ layoutPosts(postElementsCache);
+ }
+});
+
+fetchPosts();
\ No newline at end of file
diff --git a/website/masonry-smooth.js b/website/masonry-smooth.js
new file mode 100644
index 0000000..6a8cc44
--- /dev/null
+++ b/website/masonry-smooth.js
@@ -0,0 +1,53 @@
+(function() {
+ let currentLayout = null;
+ let posts = [];
+ let columns = [];
+ let originalPositions = new Map();
+
+ function initMasonry() {
+ const postsContainer = document.getElementById('posts');
+ if (!postsContainer) return;
+
+ posts = Array.from(postsContainer.querySelectorAll('.post'));
+ columns = Array.from(postsContainer.querySelectorAll('.masonry-col'));
+
+ if (posts.length === 0 || columns.length === 0) return;
+
+ // Store original positions of posts
+ posts.forEach((post, index) => {
+ const rect = post.getBoundingClientRect();
+ originalPositions.set(post, {
+ originalParent: post.parentNode,
+ originalIndex: index,
+ originalRect: rect
+ });
+ });
+
+ checkLayout();
+ }
+
+ function checkLayout() {
+ const isMobile = window.innerWidth <= 799;
+ const newLayout = isMobile ? 'mobile' : 'desktop';
+
+ if (newLayout !== currentLayout) {
+ currentLayout = newLayout;
+ // Don't redistribute - just let CSS media queries handle it
+ // The layout change is handled purely by CSS
+ }
+ }
+
+ // Initialize when DOM is ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initMasonry);
+ } else {
+ initMasonry();
+ }
+
+ // Listen for resize events but don't do anything that affects layout
+ let resizeTimeout;
+ window.addEventListener('resize', () => {
+ clearTimeout(resizeTimeout);
+ resizeTimeout = setTimeout(checkLayout, 150);
+ });
+})();
diff --git a/website/privacy.html b/website/privacy.html
index 6a793b0..2f1f215 100644
--- a/website/privacy.html
+++ b/website/privacy.html
@@ -66,6 +66,8 @@
+
+
Privacy Policy - Litruv
diff --git a/website/style.css b/website/style.css
index d382aee..f0c905c 100644
--- a/website/style.css
+++ b/website/style.css
@@ -1,9 +1,40 @@
+:root {
+ --main-bg: #242424;
+ --main-fg: #f0f0f0;
+ --accent: #2563eb;
+ --like: #db2777;
+ --github: #181717;
+ --youtube: #FF0000;
+ --steam: #1b2838;
+ --discord: #7289da;
+ --bluesky: #3E5BFF;
+ --post-bg: #fff;
+ --post-border: #000;
+ --avatar-bg: #f0f0f0;
+ --avatar-border: #000;
+ --shadow-light: rgba(0, 0, 0, 0.1);
+ --shadow-heavy: rgba(0, 0, 0, 1);
+ --shadow-mid: rgba(0, 0, 0, 0.05);
+ --header-shadow1: #ffffff52;
+ --header-shadow2: #000;
+}
+
+html {
+ box-sizing: border-box;
+ scroll-behavior: smooth;
+}
+
+*, *::before, *::after {
+ box-sizing: inherit;
+}
+
body {
margin: 0;
min-height: 100vh;
- background: #242424;
+ background: var(--main-bg);
font-family: monospace;
background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h20v20H0V0zm2 2v16h16V2H2z' fill='%23000000' fill-opacity='0.05'/%3E%3C/svg%3E");
+ overflow-x: hidden;
}
a {
@@ -20,25 +51,29 @@ a {
}
.container {
- max-width: 600px;
+ max-width: 1200px; /* set max width */
+ width: 100%;
margin: 0 auto;
- padding: 2rem;
+ padding: 2rem 0 2rem 0;
position: relative;
z-index: 1;
- padding-top: 100px;
+ padding-top: 160px; /* increased from 100px */
+ box-sizing: border-box;
+ overflow: visible; /* ensure expanded content is visible */
}
header {
text-align: center;
margin-bottom: 80px;
- color: #f0f0f0;
+ color: var(--main-fg);
font-size: 20px;
+ padding-top: 2rem; /* add extra space above header */
+ overflow: visible;
}
-
h1 {
transition: all 0.3s ease;
- text-shadow: 2px 2px 0 #ffffff52, 3px 3px 0 #000;
+ text-shadow: 2px 2px 0 var(--header-shadow1), 3px 3px 0 var(--header-shadow2);
margin-bottom: 0.5rem;
transform-origin: center;
cursor: pointer;
@@ -53,24 +88,129 @@ h1 {
.social-links a {
margin: 0 10px;
- color: #f0f0f0;
+ color: var(--main-fg);
text-decoration: none;
font-weight: bold;
+ font-size: 2rem;
+ display: inline-block;
}
.social-links a:hover {
- color: #2563eb;
+ color: var(--accent);
+ transform: scale(1.1);
+ transition: transform 0.3s ease;
+ text-shadow: 0 0 3px white;
+}
+
+.social-links a:hover .fab.fa-github {
+ color: var(--github);
+}
+
+social-links a:hover .fab.fa-youtube {
+ color: var(--youtube);
+}
+
+.social-links a:hover .fab.fa-steam {
+ color: var(--steam);
+}
+
+.social-links a:hover .fab.fa-discord {
+ color: var(--discord);
+}
+
+.social-links a:hover .fa-brands.fa-bluesky {
+ color: var(--bluesky);
+}
+
+#posts {
+ max-width: 1200px;
+ width: 100%;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 2rem;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ padding-left: 80px; /* Increase left padding to balance the negative margin */
+ padding-right: 40px;
+ box-sizing: border-box;
+ text-align: left;
+ min-height: 0; /* Prevent flex container from causing jumps */
+ /* Add stable height to prevent jumps */
+ contain: layout style;
+}
+
+.masonry-col {
+ flex: 1 1 0;
+ min-width: 320px;
+ max-width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 0; /* Remove gap so negative margins overlap */
+ transition: none; /* Prevent transitions during layout changes */
+ will-change: auto; /* Prevent unnecessary layer promotion */
+ /* Prevent reflow during resize */
+ contain: layout;
+}
+
+@media (max-width: 799px) {
+ #posts {
+ flex-direction: column;
+ gap: 0;
+ transition: none; /* Prevent transitions during layout changes */
+ /* Remove the min-height that was causing issues */
+ min-height: auto;
+ /* Use layout containment to minimize reflow */
+ contain: layout style size;
+ }
+ .masonry-col {
+ min-width: 0;
+ width: 100%;
+ /* Prevent content reflow during breakpoint change */
+ contain: layout style;
+ /* Hide overflow columns on mobile */
+ }
+ .masonry-col:not(:first-child) {
+ display: none;
+ }
}
.post {
- background: white;
- padding: 1.5rem;
- margin-bottom: 2rem;
- border: 2px solid #000;
+ background: var(--post-bg);
+ padding: 2rem 1.5rem;
+ margin-bottom: -0.2rem;
+ margin-top: -0.2rem;
+ margin-left: -40px;
+ margin-right: -40px;
+ border: 2px solid var(--post-border);
position: relative;
- box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.1), 6px 6px 0 rgba(0, 0, 0, 0.05);
+ box-shadow: 3px 3px 0 var(--shadow-light), 6px 6px 0 var(--shadow-mid);
transition: transform 0.35s cubic-bezier(0.68, -0.55, 0.27, 1.55),
- box-shadow 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
+ box-shadow 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55),
+ opacity 0.2s ease;
+ display: inline-block;
+ width: 100%;
+ min-width: 320px;
+ break-inside: avoid;
+ z-index: 1;
+
+ /* Dynamic rotation based on height */
+ --post-height: 300px; /* Default fallback */
+ --min-rotation: 0.5deg; /* Minimum rotation for tall posts */
+ --max-rotation: 3deg; /* Maximum rotation for short posts */
+ --rotation-factor: clamp(var(--min-rotation), calc(var(--max-rotation) - (var(--post-height) - 200px) * 0.005), var(--max-rotation));
+
+ /* Apply rotation with random direction */
+ transform: rotate(var(--rotation-factor));
+}
+
+/* Alternate rotation directions for visual variety */
+.post:nth-child(odd) {
+ transform: rotate(calc(-1 * var(--rotation-factor)));
+}
+
+.post:nth-child(even) {
+ transform: rotate(var(--rotation-factor));
}
.post-header {
@@ -82,14 +222,14 @@ h1 {
.avatar {
width: 48px;
height: 48px;
- background: #f0f0f0;
- border: 2px solid black;
+ background: var(--avatar-bg);
+ border: 2px solid var(--avatar-border);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
transform: rotate(-2deg);
- box-shadow: 2px 2px 0 #000;
+ box-shadow: 2px 2px 0 var(--avatar-border);
}
.post-content {
@@ -98,11 +238,11 @@ h1 {
.image-placeholder {
margin-top: 1rem;
- border: 4px solid black;
- background: #f0f0f0;
+ border: 4px solid var(--post-border);
+ background: var(--avatar-bg);
text-align: center;
transform: rotate(1deg);
- box-shadow: 4px 4px 0 #000;
+ box-shadow: 4px 4px 0 var(--post-border);
}
.post-actions {
@@ -122,12 +262,12 @@ h1 {
}
.action-btn.like {
- color: #db2777;
+ color: var(--like);
transform: rotate(-1deg);
}
.action-btn.repost {
- color: #2563eb;
+ color: var(--accent);
transform: rotate(1deg);
}
@@ -137,51 +277,16 @@ h1 {
display: block;
}
-
.post:hover {
- transform: scale(1.1) translateY(-5px) !important;
- box-shadow: 6px 6px 30px rgba(0, 0, 0, 1);
+ transform: scale(1.1) translateY(-5px) rotate(0deg) !important;
+ box-shadow: 6px 6px 30px var(--shadow-heavy);
+ z-index: 10;
}
.subtext {
margin-bottom: 30px;
}
-.social-links a {
- margin: 0 10px;
- color: #f0f0f0;
- font-size: 2rem;
- text-decoration: none;
- display: inline-block;
-}
-
-.social-links a:hover {
- transform: scale(1.1);
- transition: transform 0.3s ease;
- text-shadow:
- 0 0 3px white,
-}
-
-.social-links a:hover .fab.fa-github {
- color: #181717;
-}
-
-.social-links a:hover .fab.fa-youtube {
- color: #FF0000;
-}
-
-.social-links a:hover .fab.fa-steam {
- color: #1b2838;
-}
-
-.social-links a:hover .fab.fa-discord {
- color: #7289da;
-}
-
-.social-links a:hover .fa-brands.fa-bluesky {
- color: #3E5BFF;
-}
-
@keyframes shake {
0%,
@@ -209,11 +314,11 @@ h1:hover {
shake 0.5s ease-in-out infinite;
}
-.social-links a {
+social-links a {
position: relative;
}
-.social-links a::after {
+social-links a::after {
content: attr(title);
position: absolute;
bottom: 100%;
@@ -229,7 +334,7 @@ h1:hover {
white-space: nowrap;
}
-.social-links a:hover::after {
+social-links a:hover::after {
opacity: 1;
}
@@ -242,8 +347,8 @@ h1:hover {
display: inline-block;
margin: 0.5rem 0;
padding: 0.5rem 1rem;
- background: white;
- color: #242424;
+ background: var(--post-bg);
+ color: var(--main-bg);
text-decoration: none;
font-weight: bold;
border-radius: 20px;
@@ -251,8 +356,8 @@ h1:hover {
}
.project-links a:hover {
- background: #2563eb;
- color: white;
+ background: var(--accent);
+ color: var(--post-bg);
}
footer {
@@ -260,14 +365,15 @@ footer {
margin-top: 3rem;
margin-bottom: 2rem;
font-size: 0.9rem;
+ color: var(--main-fg);
}
footer a {
- color: #f0f0f0;
+ color: var(--main-fg);
text-decoration: underline;
}
footer a:hover {
- color: #2563eb;
+ color: var(--accent);
}
diff --git a/website/viewport-scroll-fix.js b/website/viewport-scroll-fix.js
new file mode 100644
index 0000000..4a8ad38
--- /dev/null
+++ b/website/viewport-scroll-fix.js
@@ -0,0 +1,50 @@
+(function() {
+ let scrollPosition = 0;
+ let isResizing = false;
+ let resizeTimeout;
+
+ function preserveScroll() {
+ if (!isResizing) {
+ scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
+ isResizing = true;
+
+ // Temporarily disable smooth scrolling during resize
+ document.documentElement.style.scrollBehavior = 'auto';
+ }
+
+ clearTimeout(resizeTimeout);
+ resizeTimeout = setTimeout(() => {
+ // Force restore scroll position multiple times to combat layout changes
+ const restoreScroll = () => {
+ window.scrollTo(0, scrollPosition);
+ };
+
+ restoreScroll();
+ requestAnimationFrame(restoreScroll);
+ setTimeout(restoreScroll, 10);
+ setTimeout(restoreScroll, 50);
+
+ setTimeout(() => {
+ // Re-enable smooth scrolling
+ document.documentElement.style.scrollBehavior = 'smooth';
+ isResizing = false;
+ }, 100);
+ }, 100);
+ }
+
+ // Listen for resize events
+ window.addEventListener('resize', preserveScroll);
+
+ // Listen for orientation changes
+ window.addEventListener('orientationchange', () => {
+ preserveScroll();
+ // Additional delay for orientation changes
+ setTimeout(() => {
+ window.scrollTo(0, scrollPosition);
+ }, 500);
+ });
+
+ // Also intercept media query changes that affect layout
+ const mediaQuery = window.matchMedia('(max-width: 799px)');
+ mediaQuery.addListener(preserveScroll);
+})();