this repo has no descr,ription vt3e.cat

feat: about page

vt3e.cat 0229495b 50a65f89

verified
+800 -38
+104 -4
pkgs/web/src/router/index.ts
··· 1 1 import type { VNode } from 'vue' 2 2 import { createRouter, createWebHistory } from 'vue-router' 3 - import { IconFolderOutlineRounded, IconLaptopWindowsOutlineRounded, IconAutoAwesomeMosaicOutline } from '@iconify-prerendered/vue-material-symbols' 3 + import { 4 + IconFolderOutlineRounded, 5 + IconLaptopWindowsOutlineRounded, 6 + IconAutoAwesomeMosaicOutline 7 + } from '@iconify-prerendered/vue-material-symbols' 4 8 5 9 import { AccentColour } from '@/stores/theme' 6 10 import { useUIStore } from '@/stores/ui' 7 11 8 12 const HomeView = () => import('../views/HomeView.vue') 9 13 const ProjectsView = () => import('../views/ProjectsView.vue') 14 + const AboutView = () => import('../views/AboutView.vue') 10 15 11 16 declare module 'vue-router' { 12 17 interface RouteMeta { ··· 19 24 } 20 25 } 21 26 27 + const FocusMap = new Map<string, string>() 28 + 29 + function isFocusable(el: HTMLElement): boolean { 30 + const focusableSelectors = 'a[href],button,textarea,input,select,[tabindex]' 31 + return el.matches(focusableSelectors) 32 + } 33 + 34 + function makeRestoreSelectorFor(el: Element | null): string | null { 35 + if (!el || !(el instanceof HTMLElement)) return null 36 + if (el.id) return `#${CSS.escape(el.id)}` 37 + 38 + const selectorParts: string[] = [] 39 + let currentEl: HTMLElement | null = el 40 + 41 + while (currentEl && currentEl !== document.body) { 42 + let part = currentEl.tagName.toLowerCase() 43 + 44 + if (currentEl.classList.length > 0) { 45 + part += '.' + Array.from(currentEl.classList) 46 + .map(cls => CSS.escape(cls)) 47 + .join('.') 48 + } 49 + 50 + const parent = currentEl.parentElement as HTMLElement 51 + if (parent) { 52 + const siblings = Array.from(parent.children) 53 + .filter(child => child.tagName === currentEl!.tagName) 54 + 55 + if (siblings.length > 1) { 56 + const index = siblings.indexOf(currentEl) + 1 57 + part += `:nth-of-type(${index})` 58 + } 59 + } 60 + 61 + selectorParts.unshift(part) 62 + if (isFocusable(currentEl)) break 63 + 64 + currentEl = parent 65 + } 66 + 67 + const selector = selectorParts.join(' > ') 68 + return isFocusable(el) ? selector : null 69 + } 70 + 71 + function focusElementBySelector(selector?: string | null): boolean { 72 + if (!selector) return false 73 + 74 + const el = document.querySelector(selector) as HTMLElement | null 75 + if (!el) return false 76 + 77 + const hadTab = el.hasAttribute('tabindex') 78 + const prevTab = el.getAttribute('tabindex') 79 + 80 + if (!el.matches('a[href],button,textarea,input,select,[tabindex]')) el.setAttribute('tabindex', '-1') 81 + el.focus({ preventScroll: false }) 82 + 83 + if (!hadTab) el.removeAttribute('tabindex') 84 + else if (prevTab !== null) el.setAttribute('tabindex', prevTab) 85 + 86 + return true 87 + } 88 + 22 89 const router = createRouter({ 23 90 history: createWebHistory(import.meta.env.BASE_URL), 24 91 routes: [ ··· 28 95 component: HomeView 29 96 }, 30 97 { 98 + path: '/about', 99 + name: 'about', 100 + component: AboutView, 101 + meta: { 102 + title: 'about', 103 + bg: AccentColour.Teal 104 + } 105 + }, 106 + { 31 107 path: '/projects', 32 108 name: 'projects', 33 109 component: ProjectsView, ··· 36 112 title: 'projects', 37 113 icon: IconFolderOutlineRounded(), 38 114 gridArea: 'area-projects', 39 - bg: AccentColour.Flamingo, 115 + bg: AccentColour.Flamingo 40 116 } 41 117 }, 42 118 { ··· 48 124 title: '/uses', 49 125 icon: IconLaptopWindowsOutlineRounded(), 50 126 gridArea: 'area-uses', 51 - bg: AccentColour.Mauve, 127 + bg: AccentColour.Mauve 52 128 } 53 129 }, 54 130 { ··· 60 136 title: 'gallery', 61 137 icon: IconAutoAwesomeMosaicOutline(), 62 138 gridArea: 'area-gallery', 63 - bg: AccentColour.Sapphire, 139 + bg: AccentColour.Sapphire 64 140 } 65 141 } 66 142 ], ··· 69 145 } 70 146 }) 71 147 148 + router.beforeEach((_to, from, next) => { 149 + const active = document.activeElement as Element | null 150 + 151 + if (active && from.fullPath) { 152 + const selector = makeRestoreSelectorFor(active) 153 + if (selector) { 154 + console.debug(`saving focus for ${from.fullPath} as ${selector}`) 155 + FocusMap.set(from.fullPath, selector) 156 + } 157 + } 158 + 159 + next() 160 + }) 161 + 72 162 router.afterEach((to) => { 73 163 const ui = useUIStore() 74 164 document.title = `vt3e - ${to.meta.title || to.name?.toString().toLowerCase()}` 75 165 76 166 if (to.path === '/') ui.setBack() 77 167 else if (to.meta.bg) ui.setForward(to.meta.bg) 168 + 169 + const selector = FocusMap.get(to.fullPath) 170 + if (selector) { 171 + requestAnimationFrame(() => { 172 + if (focusElementBySelector(selector)) { 173 + console.debug(`restoring focus for ${to.fullPath} to ${selector}`) 174 + FocusMap.delete(to.fullPath) 175 + } 176 + }) 177 + } 78 178 }) 79 179 80 180 export default router
+50
pkgs/web/src/utils/links.ts
··· 1 + import BlueskyLogo from "@/assets/icons/bluesky.svg?raw"; 2 + import DiscordLogo from "@/assets/icons/discord.svg?raw"; 3 + import TangledLogo from "@/assets/icons/tangled.svg?raw"; 4 + import GitLogo from "@/assets/icons/git.svg?raw"; 5 + 6 + export type Social = { 7 + label: string; 8 + icon: string; 9 + /** displayed icon on the home page */ 10 + homeIcon?: string; 11 + href: string; 12 + handle?: string; 13 + /** whether the handle is a "handle" or a "username"; this is shown in the ui; defaults to "handle" */ 14 + term?: "handle" | "username"; 15 + /** whether to show the link on the home page; defaults to false. */ 16 + prominent?: boolean; 17 + /** explainer about what the site is; only shown in the about view */ 18 + about?: string; 19 + /** additional note to show alongside the link; only shown in the about view */ 20 + note?: string; 21 + }; 22 + 23 + export const DID = "did:plc:2hcnfmbfr4ucfbjpnvjqvt3e"; 24 + 25 + export const SOCIALS: Social[] = [ 26 + { 27 + label: "Bluesky", 28 + href: `https://bsky.app/profile/${DID}`, 29 + handle: "vt3e.cat", 30 + icon: BlueskyLogo, 31 + prominent: true, 32 + }, 33 + { 34 + label: "Tangled", 35 + href: `https://tangled.org/${DID}`, 36 + handle: "vt3e.cat", 37 + icon: TangledLogo, 38 + homeIcon: GitLogo, 39 + prominent: true, 40 + about: "a git forge built upon the AT protocol.", 41 + }, 42 + { 43 + label: "Discord", 44 + href: "https://discord.com/users/1357056975812301013", 45 + handle: "vt3e.cat", 46 + icon: DiscordLogo, 47 + term: "username", 48 + note: "you may only be able to use the link if you share a server with me.", 49 + }, 50 + ];
+415
pkgs/web/src/views/AboutView.vue
··· 1 + <script setup lang="ts"> 2 + import { computed } from 'vue' 3 + import CardLayout from '@/components/Card/CardLayout.vue' 4 + import SvgComponent from '@/components/SvgComponent.vue' 5 + import ImageComponent from '@/components/ImageComponent.vue' 6 + 7 + import { SOCIALS, DID } from '@/utils/links' 8 + 9 + const didBase = computed(() => DID.slice(0, -4)) 10 + const didTail = computed(() => DID.slice(-4)) 11 + 12 + const currentYear = new Date().getFullYear() 13 + </script> 14 + 15 + <template> 16 + <CardLayout title="about vt3e"> 17 + <div class="man-page"> 18 + <div class="man-status-line" aria-hidden="true"> 19 + <span class="left">VT3E(1)</span> 20 + <span class="center">User Commands</span> 21 + <span class="right">VT3E(1)</span> 22 + </div> 23 + 24 + <main> 25 + <div class="header-grid"> 26 + <div class="header-info"> 27 + <section> 28 + <h3>NAME</h3> 29 + <p class="indent"> 30 + <strong>vt3e</strong> — alias "v[i]", a demifem {cat,rat}girl entity 31 + </p> 32 + </section> 33 + 34 + <section> 35 + <h3>SYNOPSIS</h3> 36 + <div class="indent code-block"> 37 + <span class="cmd">vt3e</span> 38 + <span class="flag">[-p it/she]</span> 39 + <span class="flag">[--ui-ux]</span> 40 + <span class="flag">[--dev]</span> 41 + <span class="flag">[--cats]</span> 42 + <span class="flag">[--yuri]</span> 43 + </div> 44 + </section> 45 + </div> 46 + 47 + <figure class="avatar-wrapper"> 48 + <ImageComponent name="avatar" :size="600" class="avatar-img" /> 49 + <figcaption class="avatar-caption">fig 1. entity visualization</figcaption> 50 + </figure> 51 + </div> 52 + 53 + <section> 54 + <h3>DESCRIPTION</h3> 55 + <p class="indent">haiii :3</p> 56 + <p class="indent"> 57 + <strong>vt3e</strong> is a demifem {cat,rat}girl thing that enjoys ui/ux design and 58 + general software development. it also has a fondness for cats & yuri! :3 59 + </p> 60 + </section> 61 + 62 + <section> 63 + <h3>ETYMOLOGY</h3> 64 + <p class="indent"> 65 + the designation "vt3e" is derived from the tail of its DID:PLC identifier: 66 + </p> 67 + <div class="indent code-block did"> 68 + {{ didBase }}<span class="highlight">[{{ didTail }}]</span> 69 + </div> 70 + 71 + <p class="indent"> 72 + adoption of a raw identifier was chosen because conventional names induce a disconnect. 73 + "vt3e" does not evoke these feelings. 74 + </p> 75 + <p class="indent"> 76 + it is pronounced "vee-tee-three-ee", but can be simplified to just "vee" or "vi" 77 + (pronounced "vee-eye", like the editor) for brevity. where does the "i" come from? 78 + magic! :3 79 + </p> 80 + </section> 81 + 82 + <section> 83 + <h3>ENVIRONMENT</h3> 84 + <dl class="indent env-list"> 85 + <dt>PRONOUNS="it/she"</dt> 86 + <dd>the addressing parameters for this entity.</dd> 87 + </dl> 88 + </section> 89 + 90 + <section> 91 + <h3>FILES</h3> 92 + <div class="indent file-list"> 93 + <div v-for="s in SOCIALS" :key="s.label" class="file-entry"> 94 + <div class="file-header"> 95 + <div class="icon-wrapper" aria-hidden="true"> 96 + <SvgComponent :icon="s.icon" /> 97 + </div> 98 + <span class="filename">{{ s.label.toLowerCase() }}</span> 99 + </div> 100 + 101 + <dl class="file-details"> 102 + <div class="detail-row url-row"> 103 + <dt>url</dt> 104 + <dd> 105 + <a 106 + v-if="s.href && s.href !== '#'" 107 + :href="s.href" 108 + target="_blank" 109 + rel="noopener noreferrer" 110 + > 111 + {{ s.href }} 112 + </a> 113 + <span v-else class="unavailable">no link available</span> 114 + </dd> 115 + </div> 116 + 117 + <div v-if="s.handle" class="detail-row handle"> 118 + <dt>{{ s.term || 'handle' }}</dt> 119 + <dd class="handle">{{ s.handle }}</dd> 120 + </div> 121 + 122 + <div v-if="s.about" class="detail-row about"> 123 + <dt aria-label="description" title="description">about</dt> 124 + <dd>{{ s.about }}</dd> 125 + </div> 126 + 127 + <div v-if="s.note" class="detail-row note"> 128 + <dt>note</dt> 129 + <dd class="note">{{ s.note }}</dd> 130 + </div> 131 + </dl> 132 + </div> 133 + </div> 134 + </section> 135 + </main> 136 + 137 + <div class="man-status-line footer" aria-hidden="true"> 138 + <span class="left">v1.0.0</span> 139 + <span class="center">{{ currentYear }}</span> 140 + <span class="right">VT3E(1)</span> 141 + </div> 142 + </div> 143 + </CardLayout> 144 + </template> 145 + 146 + <style scoped lang="scss"> 147 + .man-page { 148 + display: flex; 149 + flex-direction: column; 150 + gap: 0.5rem; 151 + font-family: monospace; 152 + } 153 + 154 + .man-status-line { 155 + display: flex; 156 + justify-content: space-between; 157 + 158 + width: 100%; 159 + font-weight: bold; 160 + text-transform: uppercase; 161 + color: hsl(var(--subtext1)); 162 + font-size: 0.9rem; 163 + user-select: none; 164 + border-bottom: 1px solid hsla(var(--surface2) / 0.3); 165 + padding-bottom: 0.5rem; 166 + 167 + &.footer { 168 + border-bottom: none; 169 + border-top: 1px solid hsla(var(--surface2) / 0.3); 170 + padding-top: 0.5rem; 171 + margin-bottom: 0; 172 + margin-top: 1rem; 173 + } 174 + } 175 + 176 + main { 177 + display: flex; 178 + flex-direction: column; 179 + gap: 1.5rem; 180 + } 181 + 182 + .header-grid { 183 + display: inline-grid; 184 + grid-template-columns: 1fr auto; 185 + gap: 2rem; 186 + 187 + @media (max-width: 800px) { 188 + grid-template-columns: 1fr; 189 + .avatar-wrapper { 190 + order: -1; 191 + margin: 0 auto 0 0; 192 + } 193 + } 194 + } 195 + 196 + .avatar-wrapper { 197 + margin: 0; 198 + 199 + display: flex; 200 + flex-direction: column; 201 + align-items: center; 202 + gap: 0.5rem; 203 + 204 + padding: 0.5rem; 205 + border: 2px dashed hsl(var(--overlay0)); 206 + background: hsla(var(--surface0) / 0.5); 207 + 208 + .avatar-img { 209 + width: 100%; 210 + max-width: 200px; 211 + height: auto; 212 + aspect-ratio: 1; 213 + object-fit: cover; 214 + filter: grayscale(100%) contrast(1.25); 215 + 216 + &:hover { 217 + filter: grayscale(0%); 218 + } 219 + } 220 + 221 + .avatar-caption { 222 + font-size: 0.75rem; 223 + color: hsl(var(--subtext0)); 224 + font-family: monospace; 225 + } 226 + } 227 + 228 + section { 229 + h3 { 230 + font-size: 1.05rem; 231 + font-weight: 800; 232 + margin-bottom: 0.5rem; 233 + color: hsl(var(--accent)); 234 + text-transform: uppercase; 235 + letter-spacing: 0.5px; 236 + } 237 + } 238 + 239 + .indent { 240 + margin-left: 3rem; 241 + max-width: 65ch; 242 + 243 + @media (max-width: 600px) { 244 + margin-left: 1rem; 245 + } 246 + } 247 + 248 + p { 249 + font-size: 0.95rem; 250 + line-height: 1.6; 251 + color: hsl(var(--text)); 252 + margin-bottom: 0.75rem; 253 + } 254 + 255 + .code-block { 256 + color: hsl(var(--text)); 257 + word-break: break-all; 258 + 259 + .cmd { 260 + font-weight: bold; 261 + color: hsl(var(--accent)); 262 + } 263 + .flag { 264 + margin-left: 0.75rem; 265 + color: hsl(var(--subtext1)); 266 + } 267 + .highlight { 268 + color: hsl(var(--accent)); 269 + font-weight: bold; 270 + } 271 + &.did { 272 + margin-left: 4rem; 273 + margin-bottom: 1.5rem; 274 + color: hsl(var(--subtext0)); 275 + } 276 + } 277 + 278 + .env-list { 279 + display: grid; 280 + grid-template-columns: max-content 1fr; 281 + gap: 0.5rem 2rem; 282 + 283 + dt { 284 + font-weight: bold; 285 + color: hsl(var(--text)); 286 + } 287 + dd { 288 + color: hsl(var(--subtext0)); 289 + } 290 + } 291 + 292 + .file-list { 293 + display: flex; 294 + flex-direction: column; 295 + gap: 1.5rem; 296 + padding-top: 0.75rem; 297 + 298 + .file-entry { 299 + display: flex; 300 + flex-direction: column; 301 + gap: 0.5rem; 302 + 303 + margin: -0.5rem; 304 + padding: 0.5rem; 305 + 306 + .file-header { 307 + display: flex; 308 + align-items: center; 309 + gap: 0.75rem; 310 + 311 + .icon-wrapper { 312 + width: 1.25rem; 313 + height: 1.25rem; 314 + color: hsl(var(--accent)); 315 + display: flex; 316 + align-items: center; 317 + justify-content: center; 318 + } 319 + 320 + .filename { 321 + font-weight: bold; 322 + font-size: 1rem; 323 + color: hsl(var(--text)); 324 + } 325 + } 326 + 327 + .file-details { 328 + margin-left: 2rem; 329 + display: grid; 330 + gap: 0.25rem; 331 + 332 + .detail-row { 333 + display: grid; 334 + grid-template-columns: 5rem 1fr; 335 + gap: 1rem; 336 + 337 + dt { 338 + color: hsl(var(--subtext1)); 339 + font-weight: bold; 340 + text-align: right; 341 + } 342 + 343 + &.url-row dd { 344 + word-break: none; 345 + white-space: nowrap; 346 + overflow: hidden; 347 + text-overflow: ellipsis; 348 + } 349 + 350 + dd { 351 + color: hsl(var(--subtext0)); 352 + 353 + a { 354 + color: hsl(var(--accent)); 355 + text-decoration: underline; 356 + text-decoration-color: transparent; 357 + word-break: break-all; 358 + outline: none; 359 + 360 + &:hover, 361 + &:focus-visible { 362 + text-decoration-thickness: 2px; 363 + text-decoration-color: hsl(var(--accent)); 364 + color: hsl(var(--accent)); 365 + } 366 + } 367 + 368 + &.handle { 369 + color: hsl(var(--text)); 370 + font-weight: 600; 371 + } 372 + 373 + &.note { 374 + font-style: italic; 375 + opacity: 0.8; 376 + } 377 + 378 + .unavailable { 379 + font-style: italic; 380 + color: hsl(var(--overlay2)); 381 + } 382 + 383 + &.about { 384 + text-decoration: underline; 385 + } 386 + } 387 + } 388 + } 389 + 390 + outline-offset: 0; 391 + border-radius: 0.5rem; 392 + 393 + &:hover { 394 + background: hsla(var(--surface0) / 0.5); 395 + } 396 + &:has(a:focus-visible) { 397 + outline: 0.25rem solid hsl(var(--accent)); 398 + background: hsla(var(--surface0) / 0.5); 399 + } 400 + } 401 + } 402 + 403 + @media (max-width: 700px) { 404 + .env-list { 405 + grid-template-columns: 1fr; 406 + gap: 0.25rem; 407 + dt { 408 + margin-top: 0.5rem; 409 + } 410 + dd { 411 + padding-left: 1rem; 412 + } 413 + } 414 + } 415 + </style>
+231 -34
pkgs/web/src/views/HomeView.vue
··· 1 1 <script setup lang="ts"> 2 2 import { computed } from 'vue' 3 3 import { useRouter } from 'vue-router' 4 - import { 5 - IconWavingHandOutlineRounded, 6 - IconArrowOutwardRounded, 7 - } from '@iconify-prerendered/vue-material-symbols' 4 + import { IconArrowOutwardRounded } from '@iconify-prerendered/vue-material-symbols' 5 + 6 + import SvgComponent from '@/components/SvgComponent.vue' 8 7 import ImageComponent from '@/components/ImageComponent.vue' 8 + import { SOCIALS } from '@/utils/links' 9 9 10 10 const router = useRouter() 11 11 ··· 18 18 ...r.meta, 19 19 })) 20 20 }) 21 + 22 + const socials = SOCIALS.filter((s) => s.prominent) 21 23 </script> 22 24 23 25 <template> 24 26 <div class="view-container center-content"> 25 27 <div class="bento-grid"> 26 28 <div class="card area-home"> 27 - <div> 28 - <component class="icon" :is="IconWavingHandOutlineRounded" /> 29 - <ImageComponent name="avatar" :size="480" /> 29 + <div class="profile-header"> 30 + <div class="avatar-wrapper"> 31 + <ImageComponent name="avatar" :size="480" class="avatar-img" /> 32 + </div> 33 + 34 + <div class="profile-identity"> 35 + <div class="name-row"> 36 + <h1>vt3e</h1> 37 + <span class="pronouns">it/she</span> 38 + </div> 39 + <p class="tagline">demifem <span>{cat,rat}girl</span> thing.</p> 40 + <p class="sub-tagline">hi!! i do ui/ux design & general software dev :3</p> 41 + </div> 42 + </div> 43 + 44 + <div class="profile-footer"> 45 + <div class="social-row"> 46 + <a 47 + v-for="link in socials" 48 + :key="link.label" 49 + :href="link.href || '#'" 50 + :target="link.href ? '_blank' : undefined" 51 + class="social-pill" 52 + :class="{ static: !link.href }" 53 + :title="link.label" 54 + > 55 + <SvgComponent class="social-pill_icon" :icon="link.homeIcon || link.icon" /> 56 + </a> 57 + </div> 30 58 31 - <h2>placeholder!!</h2> 32 - <p>holds your place</p> 59 + <RouterLink to="/about" class="about-btn"> 60 + <span>read more</span> 61 + <IconArrowOutwardRounded class="icon" /> 62 + </RouterLink> 33 63 </div> 34 64 </div> 35 65 ··· 53 83 </template> 54 84 55 85 <style scoped lang="scss"> 86 + @use '@/styles/variables.scss' as *; 87 + 56 88 .center-content { 57 89 justify-content: center; 58 90 } 91 + .view-container { 92 + min-height: 100dvh; 93 + } 59 94 60 95 .bento-grid { 61 96 display: grid; ··· 68 103 69 104 .card { 70 105 border-radius: 2rem; 71 - padding: 2rem; 106 + padding: 1rem; 72 107 position: relative; 73 108 text-decoration: none; 74 109 75 110 display: flex; 76 111 flex-direction: column; 77 112 justify-content: space-between; 113 + outline: none; 78 114 79 115 background-color: hsla(var(--background) / 0.75); 80 116 box-shadow: 0 0 0 0.3rem hsla(var(--background) / 1); 81 117 color: hsl(var(--crust)); 82 118 background-clip: border-box; 83 119 84 - .card-arrow { 85 - position: absolute; 86 - bottom: 1.5rem; 87 - right: 1.5rem; 88 - font-size: 1.5rem; 120 + transition-timing-function: $ease-spring; 121 + transition-duration: 0.3s; 122 + 123 + &.link-card { 124 + justify-content: space-between; 125 + 126 + .card-arrow { 127 + position: absolute; 128 + bottom: 1.5rem; 129 + right: 1.5rem; 130 + font-size: 1.5rem; 131 + transition-timing-function: $ease-spring; 132 + transition-duration: 0.35s; 133 + } 134 + 135 + &:hover, 136 + &:focus-visible { 137 + box-shadow: 0 0 0 0.75rem hsla(var(--background) / 1); 138 + .card-arrow { 139 + transform: translate(0.4rem, -0.4rem); 140 + } 141 + } 142 + &:active, 143 + &.active { 144 + box-shadow: 0 0 0 0.3rem hsla(var(--background) / 1); 145 + transform: scale(0.95); 146 + } 89 147 } 90 148 91 149 h2 { ··· 94 152 } 95 153 p { 96 154 font-size: 1rem; 97 - opacity: 1; 98 - color: hsl(var(--crust)); 99 155 font-weight: 600; 100 156 } 101 157 ··· 111 167 grid-column: span 2; 112 168 background-color: hsl(var(--surface0)); 113 169 color: hsl(var(--text)); 114 - p { 170 + justify-content: space-between; 171 + } 172 + &-projects { 173 + grid-column: span 1; 174 + grid-row: span 2; 175 + } 176 + &-uses, 177 + &-gallery { 178 + grid-column: span 1; 179 + } 180 + } 181 + } 182 + 183 + .area-home { 184 + .profile-header { 185 + display: flex; 186 + gap: 1rem; 187 + align-items: center; 188 + 189 + .avatar-wrapper { 190 + flex-shrink: 0; 191 + width: 7rem; 192 + aspect-ratio: 1 / 1; 193 + overflow: hidden; 194 + 195 + .avatar-img { 196 + width: 100%; 197 + height: 100%; 198 + object-fit: cover; 199 + 200 + border-radius: var(--radius-xl); 201 + background: hsla(var(--accent) / 0.2); 202 + } 203 + } 204 + 205 + .profile-identity { 206 + display: flex; 207 + flex-direction: column; 208 + 209 + .name-row { 210 + display: flex; 211 + align-items: center; 212 + gap: 0.8rem; 213 + margin-bottom: 0.25rem; 214 + 215 + h1 { 216 + font-size: 2.5rem; 217 + font-weight: 900; 218 + line-height: 1; 219 + } 220 + 221 + .pronouns { 222 + font-weight: 700; 223 + color: hsl(var(--mauve)); 224 + font-size: 1rem; 225 + background: hsla(var(--accent) / 0.05); 226 + padding: 0.1rem 0.5rem; 227 + border-radius: var(--radius-full); 228 + user-select: none; 229 + &:hover { 230 + background: hsla(var(--accent) / 0.1); 231 + } 232 + &:active { 233 + background: hsla(var(--accent) / 0.03); 234 + } 235 + } 236 + } 237 + 238 + .tagline { 239 + font-size: 1.1rem; 240 + color: hsl(var(--text)); 241 + font-weight: 600; 242 + 243 + span { 244 + color: hsl(var(--accent)); 245 + font-weight: inherit; 246 + } 247 + } 248 + 249 + .sub-tagline { 250 + font-size: 0.95rem; 115 251 color: hsl(var(--subtext0)); 252 + font-weight: 500; 116 253 } 254 + } 255 + } 117 256 118 - img { 119 - border-radius: 1rem; 120 - width: 6rem; 121 - height: 6rem; 122 - background-color: hsla(var(--accent) / 0.25); 257 + .profile-footer { 258 + display: flex; 259 + justify-content: space-between; 260 + align-items: center; 261 + margin-top: 1rem; 262 + } 263 + 264 + .social-row { 265 + display: flex; 266 + gap: 0.5rem; 267 + 268 + .social-pill { 269 + display: flex; 270 + align-items: center; 271 + justify-content: center; 272 + background-clip: padding-box; 273 + width: 2.8rem; 274 + height: 2.8rem; 275 + border-radius: 50%; 276 + font-size: 1.25rem; 277 + 278 + &_icon { 279 + width: 1.5rem; 280 + height: 1.5rem; 281 + display: block; 123 282 } 124 283 } 125 - &-projects { 126 - grid-column: span 1; 127 - grid-row: span 2; 284 + } 285 + 286 + .social-pill, 287 + .about-btn { 288 + background: hsla(var(--surface2) / 0.5); 289 + box-shadow: 0 0 0 0.1rem hsla(var(--surface2) / 0.5); 290 + color: hsl(var(--subtext1)); 291 + outline: none; 292 + transition: all 0.35s $ease-spring; 293 + 294 + span { 295 + font-weight: 600; 128 296 } 129 - &-uses { 130 - grid-column: span 1; 297 + 298 + &:hover, 299 + &:focus-visible { 300 + background: hsla(var(--surface2) / 0.5); 301 + color: hsl(var(--accent)); 302 + box-shadow: 0 0 0 0.3rem hsla(var(--accent) / 1); 131 303 } 132 - &-gallery { 133 - grid-column: span 1; 304 + &:active, 305 + &.active { 306 + background: hsla(var(--surface2) / 0.3); 307 + box-shadow: 0 0 0 0.15rem hsla(var(--accent) / 1); 134 308 } 135 309 } 136 310 137 - &:hover { 138 - box-shadow: 0 0 0 0.6rem hsla(var(--background) / 1); 139 - .card-arrow { 140 - transform: translate(0.4rem, -0.4rem); 311 + .about-btn { 312 + display: flex; 313 + align-items: center; 314 + 315 + gap: 0.5rem; 316 + padding: 0.6rem 1.2rem; 317 + border-radius: var(--radius-md); 318 + background: hsla(var(--surface2) / 0.5); 319 + box-shadow: 0 0 0 0.1rem hsla(var(--surface2) / 0.5); 320 + font-weight: 700; 321 + text-decoration: none; 322 + font-family: monospace; 323 + 324 + .icon { 325 + margin: 0; 326 + font-size: 1.2rem; 327 + } 328 + 329 + &:hover, 330 + &:focus-visible { 331 + .icon { 332 + transform: translate(0.1rem, -0.1rem); 333 + } 141 334 } 142 335 } 143 336 } ··· 146 339 .bento-grid { 147 340 display: flex; 148 341 flex-direction: column; 342 + } 343 + 344 + .card.area-home { 345 + min-height: 16rem; 149 346 } 150 347 } 151 348 </style>