timconspicuous.neocities.org

Complete visual overhaul

+1 -1
_config.ts
··· 11 11 site.use(jsx()); 12 12 site.use(postcss()); 13 13 site.add("styles.css"); 14 - site.copy([".jpg", ".svg", ".js"]); 14 + site.copy([".jpg", ".png", ".svg", ".js"]); 15 15 site.use(simpleIcons()); 16 16 17 17 export default site;
+161
src/_components/ReadingProgress.tsx
··· 1 + export default async function ReadingProgress() { 2 + try { 3 + // Fetch data at build time 4 + const bookData = await fetchReadingProgress(); 5 + 6 + if (!bookData) { 7 + return renderNoBooks(); 8 + } 9 + 10 + return ( 11 + <div 12 + class="reading-progress-container" 13 + dangerouslySetInnerHTML={{ 14 + __html: renderBookProgress(bookData), 15 + }} 16 + /> 17 + ); 18 + } catch (error) { 19 + return ( 20 + <div 21 + class="reading-progress-container reading-progress-error" 22 + dangerouslySetInnerHTML={{ 23 + __html: renderError( 24 + error instanceof Error 25 + ? error.message 26 + : "Unknown error", 27 + ), 28 + }} 29 + /> 30 + ); 31 + } 32 + } 33 + 34 + // Helper functions (same as before) 35 + async function fetchReadingProgress() { 36 + try { 37 + const progressResponse = await fetch( 38 + "https://pds.timtinkers.online/xrpc/com.atproto.repo.listRecords?repo=did%3Aplc%3Ao6xucog6fghiyrvp7pyqxcs3&collection=social.popfeed.feed.listItem", 39 + ); 40 + 41 + if (!progressResponse.ok) { 42 + throw new Error(`API request failed: ${progressResponse.status}`); 43 + } 44 + 45 + const data = await progressResponse.json(); 46 + 47 + const booksWithProgress = data.records.filter( 48 + (record: any) => 49 + record.value.bookProgress && 50 + record.value.bookProgress.updatedAt, 51 + ); 52 + 53 + if (booksWithProgress.length === 0) { 54 + return null; 55 + } 56 + 57 + const mostRecent = booksWithProgress.reduce( 58 + (latest: any, current: any) => { 59 + const latestDate = new Date(latest.value.updatedAt); 60 + const currentDate = new Date(current.value.updatedAt); 61 + return currentDate > latestDate ? current : latest; 62 + }, 63 + ); 64 + 65 + const progress = { 66 + isbn13: mostRecent.value.identifiers.isbn13, 67 + progress: mostRecent.value.bookProgress.percent, 68 + updatedAt: mostRecent.value.bookProgress.updatedAt, 69 + totalPages: mostRecent.value.bookProgress.totalPages, 70 + currentPage: mostRecent.value.bookProgress.currentPage, 71 + }; 72 + 73 + const metadata = await fetchMetadata(progress.isbn13); 74 + 75 + return { ...progress, ...metadata }; 76 + } catch (error) { 77 + throw new Error( 78 + `Failed to fetch reading progress: ${(error as Error).message}`, 79 + ); 80 + } 81 + } 82 + 83 + async function fetchMetadata(isbn13: string) { 84 + const response = await fetch( 85 + `https://openlibrary.org/api/books?bibkeys=ISBN:${isbn13}&format=json&jscmd=data`, 86 + ); 87 + 88 + if (!response.ok) { 89 + throw new Error(`API request failed: ${response.status}`); 90 + } 91 + 92 + const data = await response.json(); 93 + const metadata = Object.values(data)[0] as any; 94 + 95 + return { 96 + title: metadata.title, 97 + author: metadata.authors[0]?.name, 98 + coverUrl: metadata.cover?.medium, 99 + }; 100 + } 101 + 102 + function formatDate(dateString: string): string { 103 + return new Date(dateString).toLocaleDateString("en-US", { 104 + month: "short", 105 + day: "numeric", 106 + year: "numeric", 107 + }); 108 + } 109 + 110 + function renderNoBooks(): string { 111 + return ` 112 + <div class="reading-progress-empty"> 113 + <p>📚 No books currently in progress</p> 114 + </div> 115 + `; 116 + } 117 + 118 + function renderError(message: string): string { 119 + return ` 120 + <p>📚 Unable to load current reading progress</p> 121 + <small>${message}</small> 122 + `; 123 + } 124 + 125 + function renderBookProgress(book: any): string { 126 + const coverImage = book.coverUrl 127 + ? `<img src="${book.coverUrl}" alt="Book cover for ${book.title}" class="book-cover" />` 128 + : '<div class="book-cover-placeholder">📖</div>'; 129 + 130 + return ` 131 + <div class="reading-progress-header"> 132 + <span>📚</span> Currently Reading 133 + </div> 134 + <div class="book-info"> 135 + ${coverImage} 136 + <div class="book-details"> 137 + <div class="book-title"> 138 + ${book.title} 139 + </div> 140 + <div class="book-author"> 141 + by ${book.author} 142 + </div> 143 + <div class="book-meta"> 144 + <span class="progress-badge">In progress</span> 145 + <span class="last-updated"> 146 + Updated ${formatDate(book.updatedAt)} 147 + </span> 148 + </div> 149 + </div> 150 + </div> 151 + <div class="progress-container"> 152 + <div class="progress-bar"> 153 + <div class="progress-fill" style="width: ${book.progress}%"></div> 154 + </div> 155 + <div class="progress-details"> 156 + <span class="progress-percent">${book.progress}%</span> 157 + <span class="progress-pages">${book.currentPage} / ${book.totalPages} pages</span> 158 + </div> 159 + </div> 160 + `; 161 + }
+43 -63
src/_includes/layout.tsx
··· 1 1 export default function Layout(data: Lume.Data) { 2 - const title = data.header?.title || data.title || "timconspicuous"; 3 - const description = data.header?.description || data.description || ""; 4 - const avatar = data.header?.avatar || "/avatar.jpg"; 5 - const footer = data.footer || ""; 6 - 7 2 return ( 8 3 <html lang={data.lang || "en"}> 9 4 <head> ··· 12 7 name="viewport" 13 8 content="width=device-width, initial-scale=1.0" 14 9 /> 15 - <title>{title}</title> 10 + <title>timconspicuous</title> 16 11 <meta name="supported-color-schemes" content="light dark" /> 17 - <meta 18 - name="theme-color" 19 - content="hsl(220, 20%, 100%)" 20 - media="(prefers-color-scheme: light)" 21 - /> 22 - <meta 23 - name="theme-color" 24 - content="hsl(220, 20%, 10%)" 25 - media="(prefers-color-scheme: dark)" 26 - /> 27 12 <link rel="stylesheet" href="/styles.css" /> 28 13 <link 29 14 rel="icon" ··· 32 17 href="/favicon.svg" 33 18 /> 34 19 <link rel="canonical" href={data.url} /> 35 - {data.extra_head?.map((item: string) => ( 36 - <div dangerouslySetInnerHTML={{ __html: item }} /> 37 - ))} 38 20 </head> 39 21 <body> 40 - <main> 41 - <header class="header"> 42 - <script 43 - dangerouslySetInnerHTML={{ 44 - __html: ` 45 - let theme = localStorage.getItem("theme") || (window.matchMedia("(prefers-color-scheme: dark)").matches 46 - ? "dark" 47 - : "light"); 48 - document.documentElement.dataset.theme = theme; 49 - function changeTheme() { 50 - theme = theme === "dark" ? "light" : "dark"; 51 - localStorage.setItem("theme", theme); 52 - document.documentElement.dataset.theme = theme; 53 - } 54 - `, 55 - }} 56 - /> 22 + <div id="tarot-app"> 23 + <main class="tarot-layout"> 24 + <div id="card-container" class="card-container"> 25 + <div class="tarot-card"> 26 + <img 27 + id="card-image" 28 + class="card-image" 29 + src="/images/tarot/0-the-fool.png" 30 + alt="The Fool" 31 + /> 32 + </div> 33 + </div> 34 + 35 + <div id="content-container" class="content-container"> 36 + <div id="content-wrapper" class="content-wrapper"> 37 + <div id="content-title" class="content-title"> 38 + </div> 39 + {data.children} 40 + </div> 41 + </div> 42 + </main> 43 + 44 + <nav class="tarot-navigation"> 57 45 <button 58 - class="button header-theme" 59 - onclick="changeTheme()" 46 + type="button" 47 + id="prev-card" 48 + class="nav-button nav-prev" 49 + aria-label="Previous card" 50 + > 51 + <span class="nav-arrow">←</span> 52 + </button> 53 + <div id="card-indicator" class="card-indicator"> 54 + <span id="current-card">0</span> 55 + </div> 56 + <button 57 + type="button" 58 + id="next-card" 59 + class="nav-button nav-next" 60 + aria-label="Next card" 60 61 > 61 - <span class="icon">◐</span> 62 + <span class="nav-arrow">→</span> 62 63 </button> 63 - {avatar && ( 64 - <img 65 - class="header-avatar" 66 - src={avatar} 67 - alt="Avatar" 68 - data-lume-transform-images="webp avif 200@2" 69 - /> 70 - )} 71 - <h1 class="header-title">{title}</h1> 72 - {description && ( 73 - <div 74 - dangerouslySetInnerHTML={{ 75 - __html: description, 76 - }} 77 - /> 78 - )} 79 - </header> 64 + </nav> 65 + </div> 80 66 81 - {data.children} 82 - </main> 83 - 84 - {footer && ( 85 - <footer dangerouslySetInnerHTML={{ __html: footer }} /> 86 - )} 87 - <script src="/scripts/reading-progress.js"></script> 67 + <script src="/scripts/tarot.js"></script> 88 68 </body> 89 69 </html> 90 70 );
src/images/tarot/0-the-fool.png

This is a binary file and will not be displayed.

src/images/tarot/1-the-magician.png

This is a binary file and will not be displayed.

src/images/tarot/10-wheel-of-fortune.png

This is a binary file and will not be displayed.

src/images/tarot/11-justice.png

This is a binary file and will not be displayed.

src/images/tarot/12-the-hanged-man.png

This is a binary file and will not be displayed.

src/images/tarot/13-death.png

This is a binary file and will not be displayed.

src/images/tarot/14-temperance.png

This is a binary file and will not be displayed.

src/images/tarot/15-the-devil.png

This is a binary file and will not be displayed.

src/images/tarot/16-the-tower.png

This is a binary file and will not be displayed.

src/images/tarot/17-the-star.png

This is a binary file and will not be displayed.

src/images/tarot/18-the-moon.png

This is a binary file and will not be displayed.

src/images/tarot/19-the-sun.png

This is a binary file and will not be displayed.

src/images/tarot/2-the-high-priestess.png

This is a binary file and will not be displayed.

src/images/tarot/20-judgement.png

This is a binary file and will not be displayed.

src/images/tarot/21-the-world.png

This is a binary file and will not be displayed.

src/images/tarot/3-the-empress.png

This is a binary file and will not be displayed.

src/images/tarot/4-the-emperor.png

This is a binary file and will not be displayed.

src/images/tarot/5-the-hierophant.png

This is a binary file and will not be displayed.

src/images/tarot/6-the-lovers.png

This is a binary file and will not be displayed.

src/images/tarot/7-the-chariot.png

This is a binary file and will not be displayed.

src/images/tarot/8-strength.png

This is a binary file and will not be displayed.

src/images/tarot/9-the-hermit.png

This is a binary file and will not be displayed.

src/images/tarot/back.png

This is a binary file and will not be displayed.

+5 -15
src/index.page.tsx
··· 1 1 import { marked } from "npm:marked"; 2 2 3 3 export const title = "tim's neocities page"; 4 - export const header = { 5 - title: "timconspicuous", 6 - description: "", 7 - avatar: "/avatar.jpg", 8 - }; 9 4 10 5 export const links = [ 11 6 { ··· 54 49 55 50 export default ({ comp }: Lume.Data) => { 56 51 return ( 57 - <div> 58 - {/*<div 59 - id="reading-progress-widget" 60 - data-reading-progress="true" 61 - style={{ 62 - padding: "1rem", 63 - }} 64 - > 65 - </div>*/} 66 - <div> 52 + <div id="tarot-content"> 53 + <div id="card-0" className="card-content"> 67 54 <comp.Linktree links={links} /> 55 + </div> 56 + <div id="card-1" className="card-content"> 57 + <comp.ReadingProgress /> 68 58 </div> 69 59 </div> 70 60 );
+338
src/scripts/tarot.js
··· 1 + class Tarot { 2 + constructor() { 3 + this.currentCard = 0; 4 + this.totalCards = 22; // 0-21 for Major Arcana 5 + this.isTransitioning = false; 6 + this.isFirstLoad = true; 7 + 8 + // Major Arcana card names and roman numerals 9 + this.cardData = [ 10 + { id: "0", name: "The Fool" }, 11 + { id: "I", name: "The Magician" }, 12 + { id: "II", name: "The High Priestess" }, 13 + { id: "III", name: "The Empress" }, 14 + { id: "IV", name: "The Emperor" }, 15 + { id: "V", name: "The Hierophant" }, 16 + { id: "VI", name: "The Lovers" }, 17 + { id: "VII", name: "The Chariot" }, 18 + { id: "VIII", name: "Strength" }, 19 + { id: "IX", name: "The Hermit" }, 20 + { id: "X", name: "Wheel of Fortune" }, 21 + { id: "XI", name: "Justice" }, 22 + { id: "XII", name: "The Hanged Man" }, 23 + { id: "XIII", name: "Death" }, 24 + { id: "XIV", name: "Temperance" }, 25 + { id: "XV", name: "The Devil" }, 26 + { id: "XVI", name: "The Tower" }, 27 + { id: "XVII", name: "The Star" }, 28 + { id: "XVIII", name: "The Moon" }, 29 + { id: "XIX", name: "The Sun" }, 30 + { id: "XX", name: "Judgement" }, 31 + { id: "XXI", name: "The World" }, 32 + ]; 33 + 34 + this.init(); 35 + } 36 + 37 + init() { 38 + this.setupEventListeners(); 39 + this.setupTouchEvents(); 40 + this.loadFromHash(); 41 + this.updateImage(); 42 + this.updateCardIndicator(); 43 + this.loadComponent(); 44 + 45 + // Initial fade-in only on first load 46 + if (this.isFirstLoad) { 47 + this.initialFadeIn(); 48 + } 49 + } 50 + 51 + initialFadeIn() { 52 + const contentContainer = document.getElementById("content-container"); 53 + const cardContainer = document.getElementById("card-container"); 54 + 55 + // Start both containers as invisible 56 + if (contentContainer) { 57 + contentContainer.style.opacity = "0"; 58 + contentContainer.style.transform = "translate(100px, -50%)"; 59 + } 60 + if (cardContainer) { 61 + cardContainer.style.opacity = "0"; 62 + cardContainer.style.transform = "translate(-100px, -50%)"; 63 + } 64 + 65 + // Fade in after a short delay 66 + setTimeout(() => { 67 + if (contentContainer) { 68 + contentContainer.style.transition = "opacity 0.8s ease-in-out"; 69 + contentContainer.style.opacity = "1"; 70 + } 71 + if (cardContainer) { 72 + cardContainer.style.transition = "opacity 0.8s ease-in-out"; 73 + cardContainer.style.opacity = "1"; 74 + } 75 + }, 100); 76 + 77 + this.isFirstLoad = false; 78 + } 79 + 80 + setupEventListeners() { 81 + // Navigation buttons 82 + document.getElementById("prev-card")?.addEventListener( 83 + "click", 84 + () => this.previousCard(), 85 + ); 86 + document.getElementById("next-card")?.addEventListener( 87 + "click", 88 + () => this.nextCard(), 89 + ); 90 + 91 + // Keyboard navigation 92 + document.addEventListener("keydown", (e) => { 93 + if (e.key === "ArrowLeft") { 94 + e.preventDefault(); 95 + this.previousCard(); 96 + } else if (e.key === "ArrowRight") { 97 + e.preventDefault(); 98 + this.nextCard(); 99 + } 100 + }); 101 + 102 + // Hash change listener 103 + globalThis.addEventListener("hashchange", () => this.loadFromHash()); 104 + } 105 + 106 + setupTouchEvents() { 107 + let startX = 0; 108 + let startY = 0; 109 + 110 + document.addEventListener("touchstart", (e) => { 111 + startX = e.touches[0].clientX; 112 + startY = e.touches[0].clientY; 113 + }); 114 + 115 + document.addEventListener("touchend", (e) => { 116 + if (!startX || !startY) return; 117 + 118 + const endX = e.changedTouches[0].clientX; 119 + const endY = e.changedTouches[0].clientY; 120 + 121 + const diffX = startX - endX; 122 + const diffY = startY - endY; 123 + 124 + // Only trigger if horizontal swipe is dominant 125 + if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) { 126 + if (diffX > 0) { 127 + this.nextCard(); 128 + } else { 129 + this.previousCard(); 130 + } 131 + } 132 + 133 + startX = 0; 134 + startY = 0; 135 + }); 136 + } 137 + 138 + loadFromHash() { 139 + const hash = globalThis.location.hash.slice(1); 140 + if (!hash) { 141 + this.currentCard = 0; 142 + this.updateHash(); 143 + return; 144 + } 145 + 146 + // Find card index 147 + const cardIndex = this.cardData.findIndex((card) => card.id === hash); 148 + 149 + if (cardIndex !== -1) { 150 + this.currentCard = cardIndex; 151 + } else { 152 + // Try parsing as number 153 + const num = parseInt(hash); 154 + if (!isNaN(num) && num >= 0 && num < this.totalCards) { 155 + this.currentCard = num; 156 + } 157 + } 158 + 159 + this.updateImage(); 160 + this.updateCardIndicator(); 161 + } 162 + 163 + updateHash() { 164 + const card = this.cardData[this.currentCard]; 165 + globalThis.location.hash = card.id; 166 + } 167 + 168 + async previousCard() { 169 + if (this.isTransitioning) return; 170 + 171 + this.currentCard = this.currentCard > 0 172 + ? this.currentCard - 1 173 + : this.totalCards - 1; 174 + await this.transitionToCard("right"); 175 + } 176 + 177 + async nextCard() { 178 + if (this.isTransitioning) return; 179 + 180 + this.currentCard = this.currentCard < this.totalCards - 1 181 + ? this.currentCard + 1 182 + : 0; 183 + await this.transitionToCard("left"); 184 + } 185 + 186 + async transitionToCard(direction) { 187 + this.isTransitioning = true; 188 + 189 + const contentContainer = document.getElementById("content-container"); 190 + const cardContainer = document.getElementById("card-container"); 191 + 192 + // Get viewport width for proper off-screen positioning 193 + const viewportWidth = globalThis.innerWidth; 194 + 195 + // Determine slide directions 196 + const slideOutDirection = direction === "left" 197 + ? `-${viewportWidth}px` 198 + : `${viewportWidth}px`; 199 + const slideInDirection = direction === "left" 200 + ? `${viewportWidth}px` 201 + : `-${viewportWidth}px`; 202 + 203 + // Store the initial transforms for precise restoration 204 + const initialContentTransform = "translate(100px, -50%)"; 205 + const initialCardTransform = "translate(-100px, -50%)"; 206 + 207 + // Reset transitions for smooth animation 208 + if (contentContainer) { 209 + contentContainer.style.transition = "transform 0.6s ease-in-out"; 210 + } 211 + if (cardContainer) { 212 + cardContainer.style.transition = "transform 0.6s ease-in-out"; 213 + } 214 + 215 + // Phase 1: Slide content out first 216 + if (contentContainer) { 217 + contentContainer.style.transform = 218 + `translateY(-50%) translateX(${slideOutDirection})`; 219 + } 220 + 221 + // Wait 150ms, then slide card out 222 + await this.wait(150); 223 + if (cardContainer) { 224 + cardContainer.style.transform = 225 + `translateY(-50%) translateX(${slideOutDirection})`; 226 + } 227 + 228 + // Wait for slide out to complete 229 + await this.wait(300); 230 + 231 + // Update content while off-screen 232 + this.updateHash(); 233 + this.updateImage(); 234 + this.updateCardIndicator(); 235 + this.loadComponent(); 236 + 237 + // Position elements on the opposite side for slide in 238 + if (contentContainer) { 239 + contentContainer.style.transition = "none"; 240 + contentContainer.style.transform = 241 + `translateY(-50%) translateX(${slideInDirection})`; 242 + } 243 + if (cardContainer) { 244 + cardContainer.style.transition = "none"; 245 + cardContainer.style.transform = 246 + `translateY(-50%) translateX(${slideInDirection})`; 247 + } 248 + 249 + // Small delay to ensure positioning is set 250 + await this.wait(50); 251 + 252 + // Phase 2: Slide card back in first 253 + if (cardContainer) { 254 + cardContainer.style.transition = "transform 0.6s ease-in-out"; 255 + cardContainer.style.transform = initialCardTransform; 256 + } 257 + 258 + // Wait 150ms, then slide content back in 259 + await this.wait(150); 260 + if (contentContainer) { 261 + contentContainer.style.transition = "transform 0.6s ease-in-out"; 262 + contentContainer.style.transform = initialContentTransform; 263 + } 264 + 265 + // Wait for slide in to complete 266 + await this.wait(400); 267 + 268 + this.isTransitioning = false; 269 + } 270 + 271 + updateImage() { 272 + const cardImage = document.getElementById("card-image"); 273 + 274 + if (!cardImage) return; 275 + 276 + // Set image source for current card (using PNG extension for pixel art) 277 + const cardName = this.cardData[this.currentCard].name.toLowerCase() 278 + .replace(/\s+/g, "-"); 279 + const newSrc = `/images/tarot/${this.currentCard}-${cardName}.png`; 280 + 281 + // Update image source and alt text 282 + cardImage.src = newSrc; 283 + cardImage.alt = this.cardData[this.currentCard].name; 284 + } 285 + 286 + updateCardIndicator() { 287 + const indicator = document.getElementById("current-card"); 288 + if (indicator) { 289 + indicator.textContent = this.cardData[this.currentCard].id; 290 + } 291 + } 292 + 293 + loadComponent() { 294 + // Hide all card content divs 295 + document.querySelectorAll(".card-content").forEach((card) => { 296 + card.classList.remove("active"); 297 + }); 298 + 299 + const currentCard = this.cardData[this.currentCard]; 300 + 301 + // Update the content title 302 + const contentTitle = document.getElementById("content-title"); 303 + if (contentTitle) { 304 + contentTitle.textContent = `${currentCard.id}: ${currentCard.name}`; 305 + } 306 + 307 + // Show the current card's content 308 + const currentCardContent = document.getElementById( 309 + `card-${this.currentCard}`, 310 + ); 311 + 312 + if (currentCardContent) { 313 + currentCardContent.classList.add("active"); 314 + } else { 315 + // Fallback content if card doesn't exist yet 316 + const contentWrapper = document.getElementById("content-wrapper"); 317 + if (contentWrapper) { 318 + // Create a new content element with the proper ID 319 + const newContent = document.createElement("div"); 320 + newContent.id = `card-${this.currentCard}`; 321 + newContent.className = "card-content active"; 322 + newContent.innerHTML = ` 323 + <p>Coming soon...</p> 324 + `; 325 + contentWrapper.appendChild(newContent); 326 + } 327 + } 328 + } 329 + 330 + wait(ms) { 331 + return new Promise((resolve) => setTimeout(resolve, ms)); 332 + } 333 + } 334 + 335 + // Initialize when DOM is loaded 336 + document.addEventListener("DOMContentLoaded", () => { 337 + new Tarot(); 338 + });
+235 -52
src/styles.css
··· 1 1 /* Lume's design system */ 2 2 @import "https://unpkg.com/@lumeland/ds@0.5.2/ds.css"; 3 3 4 - @import "./styles/button.css"; 4 + @import "./styles/linktree.css"; 5 5 @import "./styles/readingProgress.css"; 6 6 7 + * { 8 + box-sizing: border-box; 9 + } 10 + 7 11 body { 8 - display: grid; 9 - grid-template-columns: minmax(0, 500px); 10 - grid-template-rows: 1fr auto; 11 - min-height: 100vh; 12 - text-align: center; 13 - padding: max(20px, 5vh) 20px; 14 - row-gap: 20px; 15 - justify-content: center; 16 - align-content: center; 12 + margin: 0; 13 + padding: 0; 14 + min-height: 100vh; 15 + font: var(--font-body); 16 + overflow-x: hidden; 17 + background: #151021; 18 + } 19 + 20 + #tarot-app { 21 + position: relative; 22 + min-height: 100vh; 23 + display: flex; 24 + flex-direction: column; 25 + overflow: hidden; /* Prevent horizontal scroll during transitions */ 26 + } 27 + 28 + /* Background system */ 29 + #background-container { 30 + position: fixed; 31 + top: 0; 32 + left: 0; 33 + width: 100%; 34 + height: 100%; 35 + z-index: -1; 36 + } 37 + 38 + .card-background { 39 + position: absolute; 40 + top: 0; 41 + left: 0; 42 + width: 100%; 43 + height: 100%; 44 + background-size: cover; 45 + background-position: center; 46 + background-repeat: no-repeat; 47 + transition: 48 + opacity 0.8s ease-in-out, 49 + transform 0.8s ease-in-out; 50 + opacity: 0; 51 + } 52 + 53 + .card-background.active { 54 + opacity: 1; 55 + } 56 + 57 + /* Main tarot layout */ 58 + .tarot-layout { 59 + flex: 1; 60 + display: flex; 61 + align-items: center; 62 + justify-content: center; 63 + padding: 2rem; 64 + position: relative; 65 + min-height: 100vh; 66 + overflow: hidden; 67 + } 68 + 69 + .card-container { 70 + position: absolute; 71 + left: 50%; 72 + top: 50%; 73 + transform: translate(-100px, -50%); 74 + z-index: 1; 75 + } 76 + 77 + .tarot-card { 78 + width: 365px; 79 + height: 565px; 80 + perspective: 1000px; 81 + } 82 + 83 + .card-image { 84 + filter: grayscale(50%); 85 + width: auto; 86 + height: 100%; 87 + max-width: 100%; 88 + object-fit: contain; 89 + image-rendering: pixelated; 90 + image-rendering: -moz-crisp-edges; 91 + image-rendering: crisp-edges; 92 + transition: opacity 0.3s ease-in-out; 93 + position: relative; 94 + z-index: 2; 95 + } 96 + 97 + .content-container { 98 + position: absolute; 99 + right: 50%; 100 + top: 50%; 101 + transform: translate(100px, -50%); 102 + width: 365px; 103 + max-width: 90vw; 104 + z-index: 2; 105 + display: flex; 106 + flex-direction: column; 107 + align-items: flex-start; 108 + } 109 + 110 + .content-title { 111 + color: #e0e0e0; 112 + font-size: 1.5rem; 113 + font-weight: bold; 114 + margin-bottom: 10px; 115 + text-align: left; 116 + width: 100%; 117 + } 118 + 119 + .content-wrapper { 120 + background: rgba(20, 20, 20, 0.75); 121 + border: 1px solid rgba(75, 57, 118, 0.3); 122 + border-radius: 16px; 123 + padding: 1rem; 124 + box-shadow: 125 + 0 10px 40px rgba(0, 0, 0, 0.6), 126 + inset 0 1px 0 rgba(255, 255, 255, 0.1); 127 + color: #e0e0e0; 128 + width: 100%; 129 + } 130 + 131 + /* Card content styling */ 132 + .card-content { 133 + display: none; 134 + } 135 + 136 + .card-content.active { 137 + display: block; 138 + } 139 + 140 + .card-content h2 { 141 + margin-top: 0; 142 + color: #664ea0; 143 + font-size: 1.5rem; 17 144 } 18 145 19 - main { 20 - align-self: center; 146 + .card-content em { 147 + color: #664ea0; 148 + font-style: italic; 21 149 } 22 150 23 - .header { 24 - font: var(--font-body); 25 - margin-bottom: min(5vh, 100px); 26 - color: var(--color-text); 151 + /* Navigation */ 152 + .tarot-navigation { 153 + position: fixed; 154 + bottom: 2rem; 155 + left: 50%; 156 + transform: translateX(-50%); 157 + display: flex; 158 + align-items: center; 159 + gap: 2rem; 160 + z-index: 10; 161 + } 27 162 28 - p { 29 - margin: 0; 30 - text-wrap: balance; 163 + .nav-button { 164 + background: rgba(20, 20, 20, 0.8); 165 + backdrop-filter: blur(10px); 166 + border: 1px solid rgba(75, 57, 118, 0.4); 167 + border-radius: 50%; 168 + width: 50px; 169 + height: 50px; 170 + display: flex; 171 + align-items: center; 172 + justify-content: center; 173 + color: #664ea0; 174 + cursor: pointer; 175 + transition: all 0.3s ease; 176 + font-size: 1.2rem; 177 + } 31 178 32 - +p { 33 - margin-top: .5em; 34 - } 35 - } 179 + .nav-button:hover { 180 + background: rgba(48, 37, 75, 0.2); 181 + border-color: rgba(75, 57, 118, 0.6); 182 + transform: scale(1.1); 36 183 } 37 184 38 - .header-avatar { 39 - border-radius: 50%; 40 - aspect-ratio: 1; 41 - object-fit: cover; 42 - object-position: center center; 43 - width: 150px; 44 - max-width: 50vw; 185 + .nav-button:active { 186 + transform: scale(0.95); 45 187 } 46 188 47 - .header-title { 48 - font: var(--font-title); 49 - letter-spacing: var(--font-title-spacing); 50 - margin: .5em 0 0; 51 - color: var(--color-base); 189 + .nav-button:disabled { 190 + opacity: 0.5; 191 + cursor: not-allowed; 52 192 } 53 193 54 - .header-theme { 55 - position: absolute; 56 - top: 1rem; 57 - right: 1.5rem; 194 + .card-indicator { 195 + background: rgba(20, 20, 20, 0.8); 196 + backdrop-filter: blur(10px); 197 + border: 1px solid rgba(75, 57, 118, 0.4); 198 + border-radius: 20px; 199 + padding: 0.5rem 1rem; 200 + color: #664ea0; 201 + font-weight: bold; 202 + min-width: 60px; 203 + text-align: center; 58 204 } 59 205 60 - footer { 61 - font: var(--font-small); 62 - color: var(--color-dim); 206 + /* Responsive design */ 207 + @media (max-width: 768px) { 208 + .tarot-layout { 209 + padding: 1rem; 210 + } 211 + 212 + .card-container { 213 + transform: translate(-75px, -50%) scale(0.75); 214 + transform-origin: center; 215 + } 216 + 217 + .content-container { 218 + transform: translate(75px, -50%) scale(0.75); 219 + transform-origin: center; 220 + width: calc(365px * 0.75); 221 + height: auto; 222 + display: flex; 223 + flex-direction: column; 224 + justify-content: center; 225 + } 226 + 227 + .tarot-card { 228 + width: calc(365px * 0.75); 229 + height: calc(565px * 0.75); 230 + } 231 + 232 + .content-wrapper { 233 + height: auto; 234 + max-height: calc(565px * 0.75); 235 + overflow-y: auto; 236 + } 63 237 64 - >* { 65 - margin: 0; 66 - } 238 + .content-title { 239 + font-size: 1.25rem; 240 + margin-bottom: 8px; 241 + } 242 + 243 + .tarot-navigation { 244 + bottom: 1rem; 245 + gap: 1.5rem; 246 + } 67 247 68 - >*+* { 69 - margin-top: 1em; 70 - } 248 + .nav-button { 249 + width: 40px; 250 + height: 40px; 251 + font-size: 1rem; 252 + } 71 253 72 - a { 73 - color: inherit; 74 - } 75 - } 254 + .card-indicator { 255 + padding: 0.4rem 0.8rem; 256 + min-width: 50px; 257 + } 258 + }
src/styles/button.css src/styles/linktree.css