Files
website-cv/style.css
T
2026-08-09 23:42:23 +02:00

1283 lines
38 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
:root {
--ink: #171717;
--paper: #f4efe4;
--white: #fffdf8;
--yellow: #ffd84d;
--blue: #8cbcff;
--pink: #ff9fca;
--green: #a9e76c;
--purple: #b99cff;
--orange: #ff9d57;
--accent: #d75d35; /* display-size fills only: 3.3:1 on paper, fails AA as small text */
--border: 3px solid var(--ink);
--border-thin: 2px solid var(--ink);
--shadow: 7px 7px 0 var(--ink);
--shadow-sm: 4px 4px 0 var(--ink);
--radius-lg: 18px;
--radius-md: 8px;
--radius-pill: 999px;
--font: 'Space Grotesk', sans-serif;
--mono: 'DM Mono', monospace;
--nav-h: 64px;
--max-w: 1100px;
--ease: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* ── Reset ──────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; font-size: 17px; }
body {
font-family: var(--font);
background: var(--paper);
color: var(--ink);
line-height: 1.6;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
a { color: inherit; text-decoration: none; }
ul { list-style: none; }
button { font-family: var(--font); cursor: pointer; border: none; background: none; }
a:focus-visible, button:focus-visible, [data-tip]:focus-visible {
outline: 3px solid var(--ink);
outline-offset: 3px;
}
/* ── Canvas ─────────────────────────────────────────────────── */
#bgCanvas {
position: fixed;
inset: 0;
width: 100%; height: 100%;
z-index: 0;
pointer-events: none;
opacity: 0.45;
}
/* ── Nav ────────────────────────────────────────────────────── */
#header {
background: var(--paper);
border-bottom: var(--border);
position: fixed; top: 0; left: 0; right: 0;
height: var(--nav-h);
z-index: 1000;
/* Hidden until scrolled; visibility keeps hidden links unfocusable */
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
#header.scrolled {
opacity: 1;
visibility: visible;
}
nav {
max-width: var(--max-w);
margin: 0 auto;
height: 100%;
padding: 0 2rem;
display: flex; align-items: center; justify-content: space-between;
}
.nav-logo {
width: 40px; height: 40px;
border-radius: 50%;
background: var(--yellow);
border: var(--border);
display: flex; align-items: center; justify-content: center;
font-size: 0.95rem;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--ink);
}
.nav-links {
display: flex; gap: 0.1rem;
/* Sibling of #header, so it hides/shows on its own */
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
#header.scrolled + .nav-links {
opacity: 1;
visibility: visible;
}
@media (min-width: 769px) {
.nav-links {
position: fixed;
top: 0;
right: max(calc((100vw - var(--max-w)) / 2 + 2rem), 2rem);
height: var(--nav-h);
align-items: center;
z-index: 1001;
}
}
.nav-link {
font-family: var(--mono);
font-weight: 500;
font-size: 0.76rem;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 0.5rem 0.75rem;
color: var(--ink);
position: relative;
}
.nav-link::after {
content: '';
position: absolute;
bottom: 3px; left: 50%;
transform: translateX(-50%);
width: 0; height: 3px;
background: var(--ink);
transition: width 0.25s var(--ease);
}
.nav-link:hover::after,
.nav-link.active::after { width: 70%; }
.burger { display: none; flex-direction: column; gap: 5px; padding: 8px; }
.burger span { display: block; width: 22px; height: 3px; background: var(--ink); transition: all 0.3s var(--ease); }
.burger.open span:nth-child(1) { transform: rotate(45deg) translate(5.5px, 5.5px); }
.burger.open span:nth-child(2) { opacity: 0; }
.burger.open span:nth-child(3) { transform: rotate(-45deg) translate(5.5px, -5.5px); }
/* ── Scroll hint ────────────────────────────────────────────── */
.scroll-hint {
position: fixed;
left: 50%;
bottom: 1.5rem;
transform: translateX(-50%);
width: 44px; height: 44px;
border-radius: 50%;
background: var(--white);
border: var(--border);
box-shadow: var(--shadow-sm);
display: flex; align-items: center; justify-content: center;
color: var(--ink);
font-size: 0.9rem;
z-index: 900;
opacity: 0;
visibility: hidden;
transition: opacity 0.6s, visibility 0.6s, background 150ms ease;
}
.scroll-hint.visible { opacity: 1; visibility: visible; }
.scroll-hint:hover { background: var(--yellow); }
.scroll-hint.visible i { animation: hintBob 1.6s ease-in-out infinite; }
@keyframes hintBob {
0%, 100% { transform: translateY(-2px); }
50% { transform: translateY(3px); }
}
/* ── Layout ─────────────────────────────────────────────────── */
main { position: relative; z-index: 1; }
.section {
padding: 7rem 0;
position: relative;
}
.section-inner { max-width: var(--max-w); margin: 0 auto; padding: 0 2rem; }
.section-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem; }
.section-title {
font-size: clamp(2.2rem, 4vw, 3.2rem);
font-weight: 700;
letter-spacing: -0.055em;
line-height: 1;
white-space: nowrap;
}
.section-rule { flex: 1; height: 3px; background: var(--ink); }
/* ── Hero ───────────────────────────────────────────────────── */
.hero {
user-select: none;
-webkit-user-select: none;
min-height: 100vh;
display: flex; align-items: center;
padding: calc(var(--nav-h) + 1rem) 2rem 6rem;
max-width: var(--max-w);
margin: 0 auto;
gap: 4rem;
position: relative; z-index: 1;
}
.hero-content { flex: 1; }
.hero-visual { flex: 0 0 300px; display: flex; align-items: center; justify-content: center; position: relative; }
.hero-name {
font-size: clamp(2.8rem, 5.5vw, 5rem);
font-weight: 700;
line-height: 0.88;
letter-spacing: -0.06em;
margin-top: 3rem;
margin-bottom: 1rem;
color: var(--accent);
-webkit-text-stroke: 2px var(--ink);
/* drop-shadow, not text-shadow: with per-glyph spans, Chromium mispaints
stroke + text-shadow together; the filter shadows the rendered whole */
filter: drop-shadow(4px 4px 0 var(--yellow));
}
/* Per-glyph spans for the anagram. letter-spacing is zeroed inside each
box because the h1's tracking already applies once at every inline-block
boundary — inheriting it too would double-tighten the name */
.hero-name .ltr {
display: inline-block;
letter-spacing: normal;
}
.hero-chips {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
margin-left: 0.5rem;
margin-bottom: 0.75rem;
}
.hero-role {
display: flex;
gap: 1ch;
font-family: var(--mono);
font-weight: 500;
font-size: 0.9rem;
color: var(--ink);
background: var(--yellow);
border: var(--border-thin);
border-radius: var(--radius-pill);
padding: 0.35rem 0.9rem;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
}
/* Slot-machine reels: each slot is a one-line window onto a word column */
.hero-role .slot { display: block; height: 1.6em; overflow: hidden; transition: width 0.2s ease; }
.hero-role .reel { display: flex; flex-direction: column; }
/* nowrap: a hyphenated word (FULL-STACK) would otherwise line-break while
the slot is narrow mid-expansion, doubling a row's height and landing
the whole strip one row off */
.hero-role .reel span { display: block; text-align: center; white-space: nowrap; }
/* Hero social links */
.hero-links {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.hero-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1.1rem;
border-radius: var(--radius-md);
font-size: 0.85rem;
font-weight: 700;
letter-spacing: 0.03em;
border: var(--border);
background: var(--white);
box-shadow: var(--shadow-sm);
color: var(--ink);
/* No transform/box-shadow transition: interpolating a hard offset shadow
against a composited translate causes subpixel jitter — snap instead */
transition: background 150ms ease;
}
.hero-link i { color: var(--ink); font-size: 0.9rem; }
/* Oversized invisible hit area: the hover/active translate is ±2px, so
without this the edge retreats from the cursor and hover flickers.
will-change keeps the buttons on a compositor layer at rest, so applying
the hover transform doesn't re-rasterize (and shift) their contents */
.hero-link, .project-link { position: relative; will-change: transform; }
.hero-link::after, .project-link::after {
content: '';
position: absolute;
inset: -4px;
border-radius: inherit;
}
.hero-link--primary { background: var(--yellow); }
.hero-link:hover { transform: translate(-2px, -2px); box-shadow: 5px 5px 0 var(--ink); }
.hero-link:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--ink); }
.gitea-bg { fill: var(--white); }
.gitea-fg { fill: var(--ink); }
/* Avatar */
.avatar-ring {
width: 350px; height: 350px;
border-radius: 50%;
position: relative;
background: var(--white);
border: var(--border);
box-shadow: var(--shadow);
overflow: hidden;
display: flex; align-items: center; justify-content: center;
}
.avatar-placeholder {
width: 100%; height: 100%;
border-radius: 50%;
overflow: hidden;
}
.avatar-placeholder img { width: 100%; height: 100%; object-fit: cover; }
/* Spin the image inside the ring, not the ring itself: the ring's hard
offset shadow would visibly orbit if the shadowed element rotated.
The small icon circles have no offset shadow, so they spin whole. */
.avatar-ring.spinning .avatar-placeholder,
.skill-card-icon.spinning,
.project-icon.spinning,
.edu-icon.spinning,
.nav-logo.spinning { animation: avatarSpin 0.8s var(--ease); }
/* rotate is only keyed at 0%/100% so it sweeps through the scale midpoint
without easing to a stop at 180° */
@keyframes avatarSpin {
0% { rotate: 0deg; scale: 1; }
50% { scale: 1.1; }
100% { rotate: 360deg; scale: 1; }
}
@media (prefers-reduced-motion: reduce) {
.avatar-ring.spinning .avatar-placeholder,
.skill-card-icon.spinning,
.project-icon.spinning,
.edu-icon.spinning,
.nav-logo.spinning { animation: none; }
}
.hero-pronouns {
font-family: var(--mono);
font-size: 0.78rem;
color: var(--ink);
background: var(--white);
border: var(--border-thin);
border-radius: var(--radius-pill);
padding: 0.3rem 0.8rem;
letter-spacing: 0.06em;
position: relative;
overflow: hidden;
/* own stacking context, so the ::before flag layer sits between this
pill's background and its text instead of hiding behind the pill */
isolation: isolate;
}
/* Non-binary flag sweep (click easter egg): four diagonal bands slide in,
fill the pill, rest a beat, then slide out — z-index -1 paints above the
pill's background but below its content */
.hero-pronouns::before {
content: '';
position: absolute;
top: 0; bottom: 0;
left: -20%;
width: 140%;
z-index: -1;
/* Wider than the pill, fading to transparent past diagonal stops on
both ends, so the yellow and black bands keep slanted edges instead
of ending at the panel's straight sides */
background: linear-gradient(
115deg,
transparent 0 8%,
#fcf434 8% 29%,
#ffffff 29% 50%,
#9c59d1 50% 71%,
#2c2c2c 71% 92%,
transparent 92% 100%
);
transform: translateX(-101%);
}
.hero-pronouns.waving::before { animation: nbSweep 1.35s linear; }
/* Text yields while the flag holds the pill (ink would drown in the
black band), then returns as the bands leave */
.hero-pronouns.waving { animation: nbTextFade 1.35s ease; }
/* No hold: heavy deceleration into the filled position, then a creeping
re-acceleration out — a near-stop rather than a stop */
@keyframes nbSweep {
0% { transform: translateX(-101%); animation-timing-function: cubic-bezier(0.1, 0.7, 0.45, 1); }
50% { transform: translateX(0); animation-timing-function: cubic-bezier(0.55, 0, 0.9, 0.3); }
100% { transform: translateX(101%); }
}
@keyframes nbTextFade {
0% { color: var(--ink); }
20%, 80% { color: transparent; }
100% { color: var(--ink); }
}
/* ── About ──────────────────────────────────────────────────── */
.about-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3.5rem; align-items: start; }
.about-right { display: flex; flex-direction: column; gap: 1.25rem; }
.about-text {
background: var(--white);
border: var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
padding: 1.75rem;
}
.about-text p { font-size: 0.95rem; margin-bottom: 1rem; }
.about-text p:last-child { margin-bottom: 0; }
.pepsi-card {
background: var(--pink);
border: var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow);
text-align: center;
}
.pepsi-placeholder {
/* ~7:6, tuned so the card exactly levels with .about-text at full
container width (480px photo box -> 461px card) */
aspect-ratio: 41 / 35;
border-bottom: var(--border);
position: relative;
overflow: hidden;
cursor: pointer;
}
.pepsi-incoming { position: absolute; inset: 0; }
/* Clickable photos shouldn't ghost-drag or get text-selected */
.avatar-placeholder img, .pepsi-placeholder img {
-webkit-user-drag: none;
user-select: none;
-webkit-user-select: none;
}
.pepsi-placeholder img {
width: 100%; height: 100%; object-fit: cover; display: block;
}
.pepsi-caption {
padding: 0.75rem 1rem;
font-family: var(--mono);
font-weight: 500;
font-size: 0.72rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
/* ── Skills ─────────────────────────────────────────────────── */
.skill-card {
background: var(--yellow);
border: var(--border);
border-radius: var(--radius-lg);
padding: 1.75rem;
box-shadow: var(--shadow);
}
.skill-card-icon {
width: 46px; height: 46px;
border-radius: 50%;
background: var(--white);
border: var(--border);
display: flex; align-items: center; justify-content: center;
color: var(--ink); font-size: 1.1rem;
margin-bottom: 0.75rem;
}
.skill-card h3 { font-size: 1.05rem; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 1.25rem; }
/* ── Projects (list layout) ─────────────────────────────────── */
.projects-list { display: flex; flex-direction: column; gap: 2rem; }
.project-card {
background: var(--white);
border: var(--border);
border-radius: var(--radius-lg);
padding: 2rem;
box-shadow: var(--shadow);
}
.project-header {
display: flex; align-items: flex-start; gap: 1rem;
margin-bottom: 1.25rem;
}
.project-icon {
width: 46px; height: 46px; flex-shrink: 0;
border-radius: 50%;
background: var(--blue);
border: var(--border);
display: flex; align-items: center; justify-content: center;
color: var(--ink); font-size: 1.1rem;
}
.project-title-row {
flex: 1;
display: flex; align-items: center; justify-content: space-between;
flex-wrap: wrap; gap: 0.5rem;
}
.project-title-row h3 { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.03em; }
.project-ext-links { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.project-link {
display: inline-flex; align-items: center; gap: 0.5rem;
font-family: var(--mono);
font-weight: 500;
font-size: 0.72rem;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 0.35rem 0.8rem;
border-radius: var(--radius-md);
border: var(--border-thin);
background: var(--white);
color: var(--ink);
box-shadow: 3px 3px 0 var(--ink);
transition: background 150ms ease;
}
.project-link:hover { transform: translate(-2px, -2px); box-shadow: 4px 4px 0 var(--ink); background: var(--yellow); }
.project-link:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--ink); }
.project-badge {
display: inline-flex; align-items: center; gap: 0.5rem;
font-family: var(--mono);
font-weight: 500;
font-size: 0.72rem;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 0.35rem 0.8rem;
border-radius: var(--radius-pill);
background: var(--blue);
border: var(--border-thin);
color: var(--ink);
}
.project-body { margin-bottom: 1.25rem; }
.project-body p {
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.project-body p:last-child { margin-bottom: 0; }
.project-body a {
text-decoration: underline;
text-decoration-thickness: 2px;
text-underline-offset: 3px;
}
.project-tags { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.project-tags span, .timeline-tags span {
font-family: var(--mono);
font-weight: 500;
font-size: 0.72rem;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 0.3rem 0.75rem;
border-radius: var(--radius-pill);
background: var(--white);
border: var(--border-thin);
color: var(--ink);
transition: background 150ms ease;
}
#projects .project-tags span:hover,
#projects .project-tags span.tip-open { background: var(--blue); }
#experience .timeline-tags span:hover,
#experience .timeline-tags span.tip-open { background: var(--green); }
/* ── Timeline / Experience ──────────────────────────────────── */
.timeline { position: relative; padding-left: 1.75rem; }
.timeline::before {
content: ''; position: absolute; left: 0; top: 0; bottom: 0;
width: 3px; background: var(--ink);
}
.timeline-item { position: relative; padding-left: 2.25rem; padding-bottom: 3rem; }
.timeline-item:last-child { padding-bottom: 0; }
.timeline-marker {
position: absolute; left: calc(-1.75rem - 7.5px); top: 0rem;
width: 18px; height: 18px; border-radius: 50%;
background: var(--green); border: 3px solid var(--ink);
}
.timeline-card {
background: var(--white);
border: var(--border);
border-radius: var(--radius-lg);
padding: 1.75rem;
box-shadow: var(--shadow);
}
.timeline-meta { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }
.timeline-date {
font-family: var(--mono);
font-weight: 500;
font-size: 0.72rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.timeline-tag {
font-family: var(--mono);
font-weight: 500;
font-size: 0.72rem;
letter-spacing: 0.06em;
text-transform: uppercase;
background: var(--green);
border: var(--border-thin);
border-radius: var(--radius-pill);
padding: 0.3rem 0.75rem;
}
.timeline-role { font-size: 1.35rem; font-weight: 700; letter-spacing: -0.03em; margin-bottom: 0.25rem; }
.timeline-company { font-size: 0.85rem; margin-bottom: 0.75rem; }
.timeline-desc { font-size: 0.95rem; margin-bottom: 1rem; }
.timeline-tags { display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* ── Education ──────────────────────────────────────────────── */
.education-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem; }
.edu-card {
background: var(--purple);
border: var(--border);
border-radius: var(--radius-lg);
padding: 1.75rem;
box-shadow: var(--shadow);
display: flex; gap: 1.25rem; align-items: flex-start;
}
.edu-icon {
width: 46px; height: 46px; flex-shrink: 0;
border-radius: 50%;
background: var(--white);
border: var(--border);
display: flex; align-items: center; justify-content: center;
color: var(--ink); font-size: 1.1rem;
}
.edu-body h3 { font-size: 1.05rem; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 0.25rem; }
.edu-school { font-weight: 700; font-size: 0.85rem; margin-bottom: 0.25rem; }
.edu-date {
font-family: var(--mono);
font-weight: 500;
font-size: 0.72rem;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 0.75rem;
}
.edu-desc { font-size: 0.85rem; }
.edu-links { margin-top: 0.75rem; display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* ── Footer ─────────────────────────────────────────────────── */
footer {
position: relative; z-index: 1;
background: var(--ink);
/* Deliberately dim: reads as boilerplate unless you actually look */
color: rgba(255, 255, 255, 0.35);
font-family: var(--mono);
font-size: 0.72rem;
letter-spacing: 0.05em;
text-transform: uppercase;
text-align: center; padding: 1.25rem 2rem;
}
.delete-site-wrap {
position: relative; z-index: 1;
display: flex;
justify-content: center;
padding: 0 2rem 4rem;
}
.delete-site {
font-weight: 700;
font-size: 0.85rem;
letter-spacing: 0.03em;
color: #fff;
background: #c92a2a;
border: var(--border);
border-radius: var(--radius-md);
padding: 0.6rem 1.1rem;
box-shadow: var(--shadow-sm);
transition: background 150ms ease;
}
.delete-site:hover { transform: translate(-2px, -2px); box-shadow: 5px 5px 0 var(--ink); }
.delete-site:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--ink); }
/* ── Delete-website dialog ──────────────────────────────────── */
.delete-dialog {
margin: auto;
max-width: 26rem;
background: var(--white);
color: var(--ink);
border: var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
padding: 1.75rem;
font-size: 0.95rem;
}
.delete-dialog::backdrop { background: rgba(23, 23, 23, 0.55); }
.delete-dialog-title {
font-size: 1.35rem;
font-weight: 700;
letter-spacing: -0.03em;
margin-bottom: 0.5rem;
}
.delete-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.5rem;
}
.dialog-button {
font-family: var(--font);
font-weight: 700;
font-size: 0.85rem;
letter-spacing: 0.03em;
padding: 0.6rem 1.1rem;
border: var(--border);
border-radius: var(--radius-md);
background: var(--white);
color: var(--ink);
box-shadow: var(--shadow-sm);
transition: background 150ms ease;
}
.dialog-button--danger { background: #c92a2a; color: #fff; }
.dialog-button:hover { transform: translate(-2px, -2px); box-shadow: 5px 5px 0 var(--ink); }
.dialog-button:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--ink); }
/* ── Terminal (js/terminal.js, lazy-built dialog) ───────────── */
.term-dialog {
margin: auto;
width: min(720px, 92vw);
padding: 0;
/* clips the dark screen to the sticker's rounded corners */
overflow: hidden;
background: var(--white);
color: var(--ink);
border: var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.term-dialog::backdrop { background: rgba(23, 23, 23, 0.55); }
.term-dialog[open] { animation: termPop 0.22s var(--ease); }
@keyframes termPop {
from { transform: scale(0.92) translateY(10px); opacity: 0; }
to { transform: none; opacity: 1; }
}
.term-titlebar {
display: flex;
align-items: center;
justify-content: center;
padding: 0.6rem 1rem;
border-bottom: var(--border);
}
.term-title {
font-family: var(--mono);
font-weight: 500;
font-size: 0.75rem;
letter-spacing: 0.05em;
}
.term-screen {
background: var(--ink);
color: var(--paper);
font-family: var(--mono);
font-size: 0.85rem;
line-height: 1.5;
height: min(480px, 70vh);
padding: 0.9rem 1rem;
overflow-y: auto;
overscroll-behavior: contain;
cursor: text;
}
/* min-height keeps blank scrollback lines one line tall */
.term-line {
white-space: pre-wrap;
overflow-wrap: anywhere;
min-height: 1.5em;
}
.term-ps1, .term-prompt { color: var(--green); }
.term-dir { color: var(--blue); }
.term-inputrow { display: flex; gap: 1ch; align-items: baseline; }
.term-prompt { white-space: nowrap; }
.term-input {
flex: 1;
min-width: 0;
padding: 0;
font: inherit;
color: inherit;
background: none;
border: none;
outline: none;
caret-color: var(--paper);
}
/* nano takes over the whole screen; shell scrollback hides underneath */
.term-screen.nano-open {
overflow: hidden;
display: flex;
flex-direction: column;
}
.term-screen.nano-open .term-out,
.term-screen.nano-open .term-inputrow { display: none; }
.nano {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
/* reverse video, like the real thing */
.nano-bar {
background: var(--paper);
color: var(--ink);
padding: 0 0.5ch;
}
.nano-title { display: flex; justify-content: space-between; }
.nano-edit {
flex: 1;
min-height: 0;
font: inherit;
color: inherit;
background: none;
border: none;
outline: none;
resize: none;
white-space: pre;
overflow: auto;
caret-color: var(--paper);
}
.nano-status { text-align: center; min-height: 1.5em; }
.nano-status span {
background: var(--paper);
color: var(--ink);
padding: 0 1ch;
}
.nano-keys { white-space: pre-wrap; }
.nano-key {
background: var(--paper);
color: var(--ink);
padding: 0 0.5ch;
}
/* ── Responsive ─────────────────────────────────────────────── */
@media (max-width: 960px) {
.about-grid { grid-template-columns: 1fr; gap: 2rem; }
.hero-visual { display: none; }
.hero { flex-direction: column; text-align: center; padding-top: calc(var(--nav-h) + 2rem); }
.hero-links { justify-content: center; }
.hero-chips { justify-content: center; margin-left: 0; }
/* Stacked hero: the base clamp bottoms out too small; the ceiling still
keeps the longest anagram ("a joking deal") on one line at 360px */
.hero-name { font-size: clamp(3.2rem, 11vw, 4.8rem); }
}
@media (max-width: 768px) {
html { font-size: 14px; }
:root {
--shadow: 5px 5px 0 var(--ink);
--shadow-sm: 3px 3px 0 var(--ink);
--radius-lg: 14px;
}
.hero-name { -webkit-text-stroke-width: 1.5px; filter: drop-shadow(3px 3px 0 var(--yellow)); }
.nav-links {
position: fixed;
top: var(--nav-h);
left: 0; right: 0;
background: var(--paper);
flex-direction: column;
align-items: center;
padding: 1.5rem;
gap: 0.4rem;
transform: translateY(-140%);
transition: transform 0.4s var(--ease), opacity 0.3s, visibility 0.3s;
border-bottom: var(--border);
z-index: 999;
}
.nav-links.open {
transform: none;
}
.burger { display: flex; }
.education-grid { grid-template-columns: 1fr; }
.project-title-row { flex-direction: column; align-items: flex-start; }
.term-dialog { width: calc(100vw - 1.5rem); }
.term-screen { height: min(480px, 65vh); }
}
@media (max-width: 480px) {
.section-header { flex-wrap: wrap; }
.section-rule { display: none; }
.timeline { padding-left: 1rem; }
.timeline-item { padding-left: 1.5rem; }
.timeline-marker { left: calc(-1rem - 7.5px); }
}
.skills-intro {
font-size: 1.05rem;
max-width: 680px;
margin-bottom: 2rem;
background: var(--white);
border: var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
padding: 1.25rem 1.75rem;
}
.lang-cloud {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 2.5rem;
}
.lang-tag {
font-family: var(--mono);
font-weight: 500;
font-size: 0.85rem;
letter-spacing: 0.05em;
padding: 0.5rem 1.1rem;
border-radius: var(--radius-pill);
background: var(--white);
border: var(--border-thin);
color: var(--ink);
transition: background 150ms ease;
}
.lang-tag:hover,
.lang-tag:focus-visible,
.lang-tag.tip-open { background: var(--yellow); }
/* ── Skill pills: shared behavior (no select + tooltip anchor) ─── */
.lang-tag, .project-tags span, .timeline-tags span {
position: relative;
cursor: default;
user-select: none;
-webkit-user-select: none;
}
/* Tooltip: shows on hover, keyboard focus, or tap (js/tooltips.js toggles .tip-open) */
[data-tip]:not([data-tip=""]):hover::after,
[data-tip]:not([data-tip=""]):focus-visible::after,
[data-tip]:not([data-tip=""]).tip-open::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%);
width: max-content;
max-width: 220px;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-md);
background: var(--ink);
color: var(--white);
font-family: var(--font);
font-size: 0.75rem;
font-weight: 500;
line-height: 1.4;
letter-spacing: normal;
text-transform: none;
text-align: center;
white-space: normal;
z-index: 10;
pointer-events: none;
}
/* Tooltip arrow */
[data-tip]:not([data-tip=""]):hover::before,
[data-tip]:not([data-tip=""]):focus-visible::before,
[data-tip]:not([data-tip=""]).tip-open::before {
content: '';
position: absolute;
bottom: calc(100% - 3px);
left: 50%;
transform: translateX(-50%);
border: 8px solid transparent;
border-top-color: var(--ink);
z-index: 10;
pointer-events: none;
}
/* Body-text tooltips: dotted underline marks hover info
(dashed marks clickable, see .dnd-roll) */
.text-tip {
position: relative;
cursor: help;
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-thickness: 2px;
text-underline-offset: 3px;
}
/* List variant: newlines (
) in the attribute become line breaks */
[data-tip].text-tip--list:hover::after,
[data-tip].text-tip--list:focus-visible::after,
[data-tip].text-tip--list.tip-open::after {
white-space: pre-line;
text-align: left;
max-width: 300px;
}
.skills-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
@media (max-width: 600px) {
.skills-cards { grid-template-columns: 1fr; }
}
.project-body code {
font-family: var(--mono);
font-size: 0.88em;
background: #e6e0d0;
padding: 0.1em 0.4em;
border-radius: var(--radius-md);
}
/* ── D20 roll on "Dungeons & Dragons" ───────────────────────── */
.dnd-roll {
position: relative;
user-select: none;
-webkit-user-select: none;
cursor: pointer;
text-decoration: underline;
text-decoration-style: dashed;
text-decoration-thickness: 2px;
text-underline-offset: 3px;
border-radius: 4px;
padding: 0 0.15em;
margin: 0 -0.15em;
transition: background 150ms ease;
}
.dnd-roll:hover { background: var(--pink); }
.die-scene {
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
width: 110px;
height: 110px;
margin-left: -55px;
pointer-events: none;
z-index: 10;
transition: opacity 0.3s;
}
/* No perspective/preserve-3d: js/d20.js projects each face flat itself —
Firefox's 3D plane-splitting drops parts of faces at grazing angles */
.die {
position: absolute;
inset: 0;
}
.die-face {
position: absolute;
left: 50%;
top: 50%;
display: flex;
align-items: center;
justify-content: center;
/* no backface-visibility: js/d20.js culls faces itself (Firefox flicker) */
color: var(--ink);
font-weight: 700;
}
/* 6 and 9 get the disambiguating underline, like a real die */
.die-face--mark {
text-decoration: underline;
text-decoration-thickness: 0.09em;
text-underline-offset: 0.12em;
}
/* Nat-20 burst bits: DOM sprinkles that fly out above the page content */
.burst-bit {
position: fixed;
width: 5px;
height: 14px;
border-radius: var(--radius-pill);
border: 2px solid var(--ink);
z-index: 1200;
pointer-events: none;
}
/* ── Achievements (js/achievements.js, absent until first unlock) ── */
.achv {
position: fixed;
top: 0.75rem;
left: 0.75rem;
z-index: 950;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.6rem;
transition: top 0.3s var(--ease);
animation: achvPop 0.35s var(--ease);
transform-origin: top left;
}
/* Tucked below the nav whenever it's on screen */
#header.scrolled ~ .achv { top: calc(var(--nav-h) + 0.75rem); }
@keyframes achvPop {
from { transform: scale(0); }
}
.achv-badge {
display: inline-flex;
align-items: center;
gap: 0.6ch;
font-family: var(--mono);
font-size: 0.85rem;
color: var(--ink);
padding: 0.3rem 0.85rem;
background: var(--white);
border: var(--border-thin);
border-radius: var(--radius-pill);
box-shadow: var(--shadow-sm);
transition: background 150ms ease;
}
.achv-badge:hover { transform: translate(-2px, -2px); box-shadow: 5px 5px 0 var(--ink); }
.achv-badge:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--ink); }
.achv-panel {
display: flex;
flex-direction: column;
gap: 0.55rem;
}
.achv-panel[hidden] { display: none; }
/* Opening the panel marches the shields in from off-screen left,
staggered down the column (re-triggers on every un-hide) */
.achv-panel:not([hidden]) .achv-medal {
animation: medalSlide 0.3s var(--ease) backwards;
}
.achv-medal:nth-child(2) { animation-delay: 45ms; }
.achv-medal:nth-child(3) { animation-delay: 90ms; }
.achv-medal:nth-child(4) { animation-delay: 135ms; }
.achv-medal:nth-child(5) { animation-delay: 180ms; }
.achv-medal:nth-child(6) { animation-delay: 225ms; }
@keyframes medalSlide {
from { transform: translateX(-105px); }
}
/* …and back out on close, unwinding bottom-first. js/achievements.js
flips hidden after the last shield leaves */
.achv-panel.achv-panel--closing .achv-medal {
animation: medalSlideOut 0.3s var(--ease) forwards;
}
.achv-panel.achv-panel--closing .achv-medal:nth-child(1) { animation-delay: 225ms; }
.achv-panel.achv-panel--closing .achv-medal:nth-child(2) { animation-delay: 180ms; }
.achv-panel.achv-panel--closing .achv-medal:nth-child(3) { animation-delay: 135ms; }
.achv-panel.achv-panel--closing .achv-medal:nth-child(4) { animation-delay: 90ms; }
.achv-panel.achv-panel--closing .achv-medal:nth-child(5) { animation-delay: 45ms; }
.achv-panel.achv-panel--closing .achv-medal:nth-child(6) { animation-delay: 0ms; }
@keyframes medalSlideOut {
to { transform: translateX(-105px); }
}
.achv-sr {
position: absolute;
width: 1px; height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
/* Medals: heraldic shields — black until earned, blank stickers until
their art lands (see MEDALS in js/achievements.js). clip-path eats
borders, so the ink outline is a clipped ::before behind a smaller
clipped ::after face; the art is an inline SVG above both */
.achv-medal {
position: relative;
width: 80px; height: 95px;
cursor: help;
}
/* Medal tooltips: the shield's own pseudo-elements are spent on the
clip-path layers, so this is a child bubble instead of [data-tip] —
keep it visually in step with the house tooltip above */
.achv-tip {
position: absolute;
left: calc(100% + 12px);
top: 50%;
transform: translateY(-50%);
width: max-content;
max-width: 220px;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-md);
background: var(--ink);
color: var(--white);
font-size: 0.75rem;
font-weight: 500;
line-height: 1.4;
z-index: 10;
pointer-events: none;
visibility: hidden;
}
.achv-tip b { display: block; font-weight: 700; }
.achv-tip::before {
content: '';
position: absolute;
left: -13px;
top: 50%;
transform: translateY(-50%);
border: 8px solid transparent;
border-right-color: var(--ink);
}
.achv-medal:hover .achv-tip { visibility: visible; }
.achv-medal::before {
content: '';
position: absolute;
inset: 0;
background: var(--ink);
clip-path: path('M0 0 H80 V37.5 C80 70 56.25 87.5 40 95 C23.75 87.5 0 70 0 37.5 Z');
}
.achv-medal::after {
content: '';
position: absolute;
inset: 3px;
background: var(--ink);
clip-path: path('M0 0 H74 V34.5 C74 65 52 81 37 89 C22 81 0 65 0 34.5 Z');
}
.achv-medal--got::after { background: var(--white); }
/* Cat lover: Pepsi (a black cat) over the Pepsi-invoking tricolor bend */
.achv-medal--cat::after {
background: linear-gradient(135deg,
#e32636 0 50px,
var(--white) 50px 66px,
#0e4c96 66px 100%);
}
.m-cat, .m-inv, .m-hk, .m-nat, .m-adv, .m-comp {
position: absolute;
inset: 0;
width: 100%; height: 100%;
z-index: 1;
}
/* Natural: a golden burst on royal purple behind a white d20 */
.achv-medal--natural::after { background: #4b2e83; }
/* Adventurer: a sword palewise on deep gules */
.achv-medal--adventurer::after { background: #a01f2f; }
/* Completionist: a gold crown on the finish flag (checky of ink and
white, 9.25px squares — one conic tile is a 2×2 check) */
.achv-medal--completionist::after {
background: repeating-conic-gradient(var(--ink) 0% 25%, var(--white) 0% 50%)
top left / 18.5px 18.5px;
}
/* Hacker: the terminal quoted back — ink screen, glowing green prompt,
blinking cursor. Must re-override --got's white face */
.achv-medal--hacker::after { background: var(--ink); }
.m-hk-cursor { animation: hkBlink 1.1s infinite; }
@keyframes hkBlink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
/* Investigator: a semé of sprinkles under a white magnifying glass;
the lens is filled with the field color (same literal in the SVG)
so the ring reads clean */
.achv-medal--investigator::after { background: #177245; }
.achv-toast {
position: fixed;
/* Undo the [popover] UA centering (inset: 0 + margin: auto) */
inset: auto;
margin: 0;
top: 1rem;
left: 50%;
/* Parked above the viewport; js/achievements.js animates it in */
transform: translate(-50%, -140%);
z-index: 1200;
display: flex;
align-items: center;
gap: 0.8rem;
background: var(--white);
border: var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
padding: 0.6rem 1.5rem 0.6rem 0.9rem;
}
/* The tiny medal: a real 80×95 shield scaled to 0.4, so the clip-path
px coordinates and the SVG art need no small-size variants */
.achv-toast-medal {
flex: none;
width: 32px; height: 38px;
}
.achv-toast-medal .achv-medal {
display: block;
transform: scale(0.4);
transform-origin: top left;
cursor: default;
}
.achv-toast-text {
display: flex;
flex-direction: column;
text-align: left;
}
.achv-toast-kicker {
font-family: var(--mono);
font-size: 0.7rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.achv-toast-name {
font-weight: 700;
font-size: 1.05rem;
letter-spacing: -0.02em;
line-height: 1.25;
}
/* ── Reduced motion ─────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.hero-link, .project-link, .lang-tag,
.project-tags span, .timeline-tags span,
.nav-link::after, #header, .hero-role .slot { transition: none; }
.scroll-hint.visible i { animation: none; }
.term-dialog[open] { animation: none; }
.achv { animation: none; transition: none; }
.achv-panel:not([hidden]) .achv-medal,
.achv-panel.achv-panel--closing .achv-medal { animation: none; }
.m-hk-cursor { animation: none; }
}