/*
 * mpaunovic.com shared styles: design tokens, base type, homepage
 * layout. The CV page and 404 page load
 * this file first, then layer their own styles on top.
 *
 * Light and dark come from prefers-color-scheme, or from the
 * topbar toggle when the visitor has picked one; every color is a
 * token so the two schemes stay in lockstep.
 */

:root {
    --font-display: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
    --font-body: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, "Liberation Mono", monospace;

    --bg: #f7f3ec;
    --bg-soft: #efe9df;
    --ink: #211c16;
    --muted: #6f675c;
    --line: #ddd3c4;
    --accent: #b34700;

    --cell: 12px;
    --heat-0: #e9e1d3;
    --heat-1: #c3e7c0;
    --heat-2: #82cc83;
    --heat-3: #42a753;
    --heat-4: #1e7a3c;
}

/* dark palette, shared by the OS preference and the manual
 * override: data-theme light wins over the OS, data-theme dark
 * wins on a light machine */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #16130f;
        --bg-soft: #1d1915;
        --ink: #ece5d9;
        --muted: #a49b8d;
        --line: #2f2921;
        --accent: #ec8443;
        --logo-invert: 1;

        --heat-0: #262019;
        --heat-1: #2f5c3e;
        --heat-2: #3f9157;
        --heat-3: #63c574;
        --heat-4: #9ef0a7;
    }
}

:root[data-theme="dark"] {
    --bg: #16130f;
    --bg-soft: #1d1915;
    --ink: #ece5d9;
    --muted: #a49b8d;
    --line: #2f2921;
    --accent: #ec8443;
    --logo-invert: 1;

    --heat-0: #262019;
    --heat-1: #2f5c3e;
    --heat-2: #3f9157;
    --heat-3: #63c574;
    --heat-4: #9ef0a7;
}

html {
    color-scheme: light dark;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: 16px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
}

/* faint paper grain over the whole page */
body::after {
    content: "";
    position: fixed;
    inset: 0;
    pointer-events: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
    opacity: 0.05;
    z-index: 2;
}

::selection {
    background: var(--accent);
    color: var(--bg);
}

a {
    color: var(--ink);
}

a:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
}

.page {
    max-width: 72rem;
    margin: 0 auto;
    padding: clamp(1.25rem, 4vw, 3rem) clamp(1.25rem, 5vw, 4rem) 2.5rem;
    /* min-height must mirror the paddings above: the page box is
     * content-box sized, so a bare 100vh here always overflows the
     * viewport by exactly the two paddings and every page scrolls */
    min-height: calc(100vh - clamp(1.25rem, 4vw, 3rem) - 2.5rem);
    display: flex;
    flex-direction: column;
}

main {
    flex: 1;
}

/* shared small monospace label */
.kicker {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    color: var(--muted);
    margin: 0;
}

/* ---------- top bar ---------- */

.topbar {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.25rem 1.5rem;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    color: var(--muted);
    padding-bottom: 1rem;
}

.topbar a {
    color: var(--muted);
    text-decoration: none;
}

.topbar a:hover {
    color: var(--accent);
}

/* the right side of the hero title: clock over theme toggle,
 * sharing the name's baseline, the button wears no chrome */
.hero-title {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    flex-wrap: wrap;
    gap: 0.25rem 1.5rem;
}

.hero-tools {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    /* keeps the column glued to the right edge when the hero
     * title wraps on narrow screens (no-op on a single line) */
    margin-left: auto;
    gap: 0.15rem;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    color: var(--muted);
}

/* when the title wraps, the tools column is what meets the hero-row
 * separator, not the name - restore the name's bottom rhythm there */
@media (max-width: 40rem) {
    .hero-tools {
        margin-bottom: 1.25rem;
    }
}

.topbar-theme {
    font: inherit;
    letter-spacing: inherit;
    color: var(--muted);
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
}

.topbar-theme:hover {
    color: var(--accent);
}

.topbar-theme:focus-visible {
    outline: 1px solid var(--accent);
    outline-offset: 0.2em;
}

/* ---------- homepage hero ---------- */

.hero {
    padding: clamp(0.25rem, 1.5vh, 1rem) 0 clamp(1rem, 2.5vh, 1.75rem);
}

.hero-name {
    font-family: var(--font-display);
    font-weight: 400;
    font-size: clamp(2.5rem, 5vw, 3.5rem);
    line-height: 1.02;
    letter-spacing: -0.01em;
    margin: 0 0 1.25rem -0.02em;
    text-wrap: balance;
}

/* the tagline shares the rule with the links: quiet body-font prose
 * on the left, the mono link list on the right, baselines aligned -
 * the old hero row's arrangement, sized for one short line */
.hero-role {
    margin: 0;
    max-width: 34rem;
    color: var(--muted);
    font-size: 1.05rem;
}

/* the rule spans the full column with the tagline prose at its left
 * end and the links at its right end, mirroring the topbar's split
 * and counterweighting the left-anchored name */
.hero-row {
    display: flex;
    gap: 1.5rem 2.5rem;
    justify-content: space-between;
    align-items: baseline;
    flex-wrap: wrap;
    border-top: 1px solid var(--line);
    padding-top: 1.1rem;
}

.link-list {
    list-style: none;
    display: flex;
    gap: 1.5rem;
    flex-wrap: wrap;
    padding: 0;
    margin: 0;
}

.link-list a {
    font-family: var(--font-mono);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    text-decoration: none;
    border-bottom: 1px solid var(--line);
    padding-bottom: 2px;
    transition: color 0.2s ease, border-color 0.2s ease;
}

.link-list a:hover {
    color: var(--accent);
    border-color: var(--accent);
}

/* the contact opener sits in the same list as the links and wears
 * their clothes; only the tag differs */
.link-button {
    font-family: var(--font-mono);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    background: none;
    border: 0;
    border-bottom: 1px solid var(--line);
    padding: 0 0 2px;
    color: inherit;
    cursor: pointer;
    transition: color 0.2s ease, border-color 0.2s ease;
}

.link-button:hover {
    color: var(--accent);
    border-color: var(--accent);
}

/* ---------- homepage game slot ---------- */

/* the 640x480 arcade keeps its internal pixels and scales to the
 * column: 4:3 against the viewport left under the hero, capped at
 * 56rem; the 20rem floor stops very short landscape viewports
 * from collapsing the frame to nothing (those scroll instead,
 * which is the honest answer there). dvh line last: browsers
 * without it keep the vh fallback above */
.game-slot {
    width: min(56rem, 100%,
        max(20rem, calc((100vh - 320px - 12.5vh) * 4 / 3)));
    width: min(56rem, 100%,
        max(20rem, calc((100dvh - 320px - 12.5vh) * 4 / 3)));
    margin: clamp(2rem, 5vh, 4rem) auto 0.75rem;
    padding: 0.6rem;
    background: var(--bg-soft);
    border: 1px solid var(--line);
    border-radius: 10px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05),
        0 10px 30px rgba(0, 0, 0, 0.1);
    opacity: 0;
    animation: game-settle 0.8s cubic-bezier(0.2, 0.7, 0.25, 1) 0.2s forwards;
}

/* the frame resolves into view rather than travelling in: a swift
 * opacity settle, done in about a second from load, clear of the
 * game's own four-second intro that is already running beneath it */
.game-slot canvas {
    display: block;
    width: 100%;
    max-width: 100%;
    height: auto;
    image-rendering: pixelated;
    border: 1px solid var(--line);
    border-radius: 4px;
}

@keyframes game-settle {
    to {
        opacity: 1;
    }
}

.game-caption {
    display: block;
    width: fit-content;
    margin: 0.65rem auto 0;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    color: var(--muted);
    text-align: center;
    text-decoration: none;
}

.game-caption:hover {
    color: var(--accent);
    text-decoration: underline;
}

/* ---------- work grid (portfolio page) ---------- */

/* the portfolio under the hero: nine cards rendered from
 * js/projects.js by js/work.js. The section head reuses the
 * hero-row split (title left, meta note right on a shared
 * hairline), and the cards borrow the retired game bezel's
 * materials: bg panels, line hairlines, soft shadow, 10px
 * corners. */

.work {
    margin-top: clamp(1.5rem, 4vh, 3rem);
}

/* auto-fill keeps an even row of three on the desktop column, two
 * on tablets, one on phones; the grid scrolls, by design */
.work-grid {
    list-style: none;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr));
    gap: 1.25rem;
    margin: 0;
    padding: 0;
}

.work-card {
    position: relative;
    display: flex;
    flex-direction: column;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 10px;
    overflow: hidden;
    opacity: 0.85;
    transition: transform 0.3s cubic-bezier(0.2, 0.7, 0.25, 1),
        opacity 0.3s ease,
        border-color 0.25s ease, box-shadow 0.25s ease;
}

/* reveal-on-scroll (js adds these): cards start below their slot
 * and settle; the per-card stagger is an inline transition-delay
 * that js clears once the reveal lands so hover stays snappy.
 * Cards rest slightly faded and come to full ink on hover or
 * keyboard focus; touch devices rest at full opacity */
.work-card.is-away {
    opacity: 0;
    transform: translateY(16px);
}

.work-card.is-in {
    opacity: 0.85;
    transform: none;
}

.work-card:hover,
.work-card:focus-within {
    opacity: 1;
    border-color: color-mix(in oklab, var(--accent) 45%, var(--line));
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05),
        0 14px 34px rgba(0, 0, 0, 0.12);
}

@media (hover: none) {
    .work-card,
    .work-card.is-in {
        opacity: 1;
    }
}

/* ---------- card art: placeholder until real assets land ----------
 *
 * Every project ships a hue (inline --ph-h, from the data) and a
 * pattern class. The wash mixes one bright and one deep tint of
 * that hue into the site's own bg tokens, so light mode gets
 * pastel washes and dark mode gets deep tones from the same
 * rules. Patterns are pure background gradients on a ::before,
 * and a big ghost initial in the display serif stamps each card
 * as editorial rather than stock. */

.work-media,
.work-shot {
    background: linear-gradient(155deg,
        color-mix(in oklab, oklch(0.74 0.13 var(--ph-h, 26)) 42%, var(--bg-soft)),
        color-mix(in oklab, oklch(0.74 0.13 var(--ph-h, 26)) 10%, var(--bg)) 52%,
        color-mix(in oklab, oklch(0.32 0.1 var(--ph-h, 26)) 36%, var(--bg-soft)));
}

.work-media {
    position: relative;
    aspect-ratio: 16 / 10;
    overflow: hidden;
    border-bottom: 1px solid var(--line);
    --pat: color-mix(in oklab, var(--ink) 16%, transparent);
    --pat-soft: color-mix(in oklab, var(--ink) 8%, transparent);
}

/* a real cover photo fills the art area and fades its edge into the
 * card on hover the pattern photos scale; covers darken a touch to
 * keep the title legible below them */
.work-media.has-cover img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform 0.25s ease, filter 0.25s ease;
}

/* tall shots can anchor to their top edge so the card shows the
 * upper part of the screen instead of the default middle crop */
.work-media.has-cover.has-cover-top img {
    object-position: 50% 0;
}

/* desktop screenshots nudged down a touch: less of the top cut off
 * than the middle crop, without pinning flush to the edge */
.work-media.has-cover.has-cover-high img {
    object-position: 50% 35%;
}

/* logos keep their aspect ratio: fitted inside an inset area on the
 * soft token background instead of stretched to fill the whole card */
.work-media.has-cover.has-logo {
    background: var(--bg-soft);
}

.work-media.has-cover.has-logo img {
    inset: 1.5rem 4rem;
    width: auto;
    height: auto;
    max-width: calc(100% - 8rem);
    max-height: calc(100% - 3rem);
    margin: auto;
    object-fit: contain;
    transition: none;
}

/* the umg and shield wordmarks ride the theme like everything
 * else: black on the soft plate in light mode, inverted to white
 * on it in dark mode, driven by the --logo-invert token */
.work-media.has-cover.has-logo-dark,
.work-card:hover .work-media.has-cover.has-logo-dark {
    background: var(--bg-soft);
}

.work-media.has-cover.has-logo-dark img,
.work-card:hover .work-media.has-cover.has-logo-dark img,
.work-open:focus-visible ~ * .work-media.has-cover.has-logo-dark img {
    filter: invert(var(--logo-invert, 0));
}

.work-card:hover .work-media.has-cover img,
.work-open:focus-visible ~ * .work-media.has-cover img {
    transform: scale(1.04);
    filter: brightness(0.94);
}

.work-media::before {
    content: "";
    position: absolute;
    inset: 0;
    transition: transform 0.45s cubic-bezier(0.2, 0.7, 0.25, 1);
}

.work-card:hover .work-media::before,
.work-card:focus-within .work-media::before {
    transform: scale(1.045);
}

.work-ghost {
    position: absolute;
    right: 0.45rem;
    bottom: -1.7rem;
    font-family: var(--font-display);
    font-size: clamp(4.5rem, 8vw, 6.5rem);
    line-height: 1;
    letter-spacing: -0.03em;
    color: color-mix(in oklab, var(--ink) 15%, transparent);
    pointer-events: none;
    user-select: none;
}

/* one placeholder pattern per project, class from the data */
.work-media.ph-bricks::before {
    background-image:
        linear-gradient(var(--pat) 2px, transparent 2px),
        linear-gradient(90deg, var(--pat) 2px, transparent 2px);
    background-size: 54px 24px;
}

.work-media.ph-tiles::before {
    background-image: radial-gradient(var(--pat) 1.3px, transparent 1.5px);
    background-size: 18px 18px;
}

.work-media.ph-route::before {
    background-image:
        repeating-linear-gradient(48deg, transparent 0 13px, var(--pat-soft) 13px 14px, transparent 14px 34px),
        repeating-linear-gradient(132deg, transparent 0 21px, var(--pat-soft) 21px 22px, transparent 22px 52px);
}

.work-media.ph-felt::before {
    background-image: radial-gradient(var(--pat-soft) 1px, transparent 1.3px);
    background-size: 7px 7px;
}

.work-media.ph-scan::before {
    background-image: repeating-linear-gradient(180deg, var(--pat-soft) 0 1px, transparent 1px 4px);
}

.work-media.ph-rings::before {
    background-image: repeating-radial-gradient(circle at 32% 62%, var(--pat-soft) 0 1px, transparent 1px 16px);
}

.work-media.ph-disc::before {
    background-image:
        linear-gradient(115deg, color-mix(in oklab, white 14%, transparent), transparent 38%, color-mix(in oklab, white 8%, transparent) 62%, transparent 78%),
        repeating-radial-gradient(circle at 50% 42%, var(--pat-soft) 0 1px, transparent 1px 7px);
}

.work-media.ph-lattice::before {
    background-image:
        repeating-linear-gradient(60deg, transparent 0 18px, var(--pat-soft) 18px 19px, transparent 19px 38px),
        repeating-linear-gradient(-60deg, transparent 0 18px, var(--pat-soft) 18px 19px, transparent 19px 38px);
}

.work-media.ph-leaf::before {
    background-image:
        repeating-linear-gradient(115deg, transparent 0 9px, var(--pat-soft) 9px 10px, transparent 10px 26px),
        repeating-linear-gradient(65deg, transparent 0 9px, var(--pat-soft) 9px 10px, transparent 10px 26px);
}

/* the year rides the art's top-right corner: a quiet paper chip
 * stamping when the work happened, so the tags stay pure stack */
.work-year {
    position: absolute;
    top: 0.7rem;
    right: 0.7rem;
    padding: 0.16rem 0.5rem;
    border-radius: 999px;
    background: color-mix(in oklab, var(--bg) 86%, transparent);
    border: 1px solid var(--line);
    font-family: var(--font-mono);
    font-size: 0.62rem;
    letter-spacing: 0.08em;
    color: var(--muted);
}

/* the play affordance: an accent pill sitting on the art, a
 * control, not decoration; the card's stretched button below
 * carries the same action for keyboards and screen readers */
.work-badge {
    position: absolute;
    left: 0.8rem;
    bottom: 0.8rem;
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.38rem 0.85rem 0.38rem 0.7rem;
    border-radius: 999px;
    background: var(--accent);
    color: var(--bg);
    font-family: var(--font-mono);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    box-shadow: 0 2px 12px color-mix(in oklab, var(--accent) 45%, transparent);
    transition: box-shadow 0.25s ease, transform 0.25s ease;
}

.work-card:hover .work-badge,
.work-card:focus-within .work-badge {
    transform: translateY(-1px);
    box-shadow: 0 4px 20px color-mix(in oklab, var(--accent) 65%, transparent);
}

.work-badge svg {
    width: 0.62rem;
    height: 0.62rem;
    fill: currentColor;
}

.work-body {
    display: flex;
    flex-direction: column;
    flex: 1;
    padding: 0.95rem 1rem 1.05rem;
}

.work-name {
    font-family: var(--font-display);
    font-weight: 400;
    font-size: 1.3rem;
    line-height: 1.1;
    margin: 0;
}

.work-blurb {
    margin: 0.3rem 0 0;
    flex: 1;
    color: var(--muted);
    font-size: 0.9rem;
    line-height: 1.45;
}

.work-tags {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    padding: 0;
    margin: 0.65rem 0 0;
}

.work-tags li {
    font-family: var(--font-mono);
    font-size: 0.62rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--muted);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 0.14rem 0.5rem;
}

/* the whole card is the click target: a transparent button
 * stretched over it, named for its action ("Play Arkanoid",
 * "Open CDClub"); the visible content stays non-interactive */
.work-open {
    position: absolute;
    inset: 0;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    cursor: pointer;
    border-radius: inherit;
}

.work-open:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -5px;
}

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}
/* the owner's current city carries a live presence dot; one accent
 * pulse, a still dot under reduced motion, and a quiet tooltip */
.here {
    position: relative;
    display: inline-block;
    margin-left: 0.3rem;
}

.here-dot {
    display: inline-block;
    width: 0.4rem;
    height: 0.4rem;
    border-radius: 50%;
    background: var(--accent);
    animation: here-pulse 2.4s ease-out infinite;
}

@keyframes here-pulse {
    0% {
        box-shadow: 0 0 0 0 rgba(179, 71, 0, 0.45);
    }

    70%,
    100% {
        box-shadow: 0 0 0 0.45rem rgba(179, 71, 0, 0);
    }
}

.here::after {
    content: attr(data-tip);
    position: absolute;
    bottom: 1.4rem;
    left: 50%;
    transform: translateX(-50%);
    background: var(--ink);
    color: var(--bg);
    font-family: var(--font-mono);
    font-size: 0.65rem;
    padding: 0.25rem 0.5rem;
    border-radius: 0.35rem;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease;
}

.here:hover::after {
    opacity: 1;
}

/* ---------- project dialog ---------- */

/* native <dialog> in the house materials. The dialog itself has
 * zero padding so a click that reaches it (past
 * .work-modal-inner) is a backdrop click and closes; all spacing
 * lives on the inner wrapper. */

html.modal-open {
    overflow: hidden;
}

[hidden] {
    display: none !important;
}

.work-modal {
    width: min(44rem, calc(100vw - 2rem));
    max-height: calc(100vh - 3rem);
    max-height: calc(100dvh - 3rem);
    padding: 0;
    border: 1px solid var(--line);
    border-radius: 14px;
    background: var(--bg);
    color: var(--ink);
    overflow: auto;
    box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
}

/* Bullet-list briefs (dense accomplishment lists) get a wider
 * measure so long points stack into fewer lines and the dialog
 * stays shorter; prose-story modals keep the bookish 44rem above.
 * Scoped via :has() on the injected .work-modal-points list, so
 * nothing else about the other dialogs changes. */
.work-modal:has(.work-modal-points) {
    width: min(60rem, calc(100vw - 2rem));
}

.work-modal::backdrop {
    background: rgba(10, 8, 6, 0.55);
}

.work-modal[open] {
    animation: work-modal-in 0.32s cubic-bezier(0.2, 0.7, 0.25, 1);
}

.work-modal[open]::backdrop {
    animation: work-backdrop-in 0.32s ease;
}

@keyframes work-modal-in {
    from {
        opacity: 0;
        transform: translateY(18px) scale(0.985);
    }
}

@keyframes work-backdrop-in {
    from {
        opacity: 0;
    }
}

.work-modal-inner {
    padding: clamp(1.25rem, 3.5vw, 1.9rem);
}

.work-modal-head {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
}

.work-modal-title {
    font-family: var(--font-display);
    font-weight: 400;
    font-size: clamp(1.5rem, 4vw, 2rem);
    line-height: 1.05;
    letter-spacing: -0.01em;
    margin: 0.2rem 0 0;
}

.work-close {
    flex: none;
    margin-left: auto;
    width: 2.1rem;
    height: 2.1rem;
    display: grid;
    place-items: center;
    background: var(--bg-soft);
    border: 1px solid var(--line);
    border-radius: 8px;
    color: var(--muted);
    cursor: pointer;
    transition: color 0.2s ease, border-color 0.2s ease;
}

.work-close:hover {
    color: var(--accent);
    border-color: var(--accent);
}

.work-close svg {
    width: 0.8rem;
    height: 0.8rem;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
}

/* where the game will live for playable projects: the bezel's
 * heir, a fixed 4:3 dark screen. The placeholder inside is what
 * the future game loader replaces (contract in js/work.js). */
.work-stage {
    position: relative;
    width: 100%;
    max-width: 40rem;
    aspect-ratio: 4 / 3;
    margin: 1.2rem auto 0.2rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    background: #0d0b09;
    border: 1px solid var(--line);
    border-radius: 6px;
}

.work-stage::after {
    content: "";
    position: absolute;
    inset: 0.5rem;
    border: 1px dashed rgba(247, 243, 236, 0.16);
    border-radius: 4px;
}

.work-stage-note {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    color: #8f8677;
}

.work-stage-dims {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 0.62rem;
    letter-spacing: 0.1em;
    color: #6b6355;
}

/* screenshot strip for non-playable projects: the same wash as
 * the card art, numbered stand-ins until real shots land */
/* screenshot zoom buttons look like plain images until hovered */
.work-shot-zoom {
    display: block;
    padding: 0;
    border: 0;
    background: none;
    cursor: zoom-in;
    width: 100%;
}

.work-shot-zoom img {
    display: block;
    width: 100%;
}

.work-shot-zoom:hover img,
.work-shot-zoom:focus-visible img {
    filter: brightness(1.08);
}

/* full-size preview: a second native dialog, naturally above the
 * project dialog in the browser's top layer. The open state is a
 * flex box so the image centers in the viewport (the default
 * block layout parks it in the top-left corner); the image
 * settles in with a small zoom instead of teleporting */
.work-lightbox {
    border: 0;
    padding: 0;
    background: transparent;
    max-width: none;
    max-height: none;
    width: 100vw;
    height: 100vh;
    cursor: zoom-out;
}

.work-lightbox[open] {
    display: flex;
    align-items: center;
    justify-content: center;
    animation: lightbox-in 0.22s ease;
}

.work-lightbox[open]::backdrop {
    animation: lightbox-backdrop-in 0.22s ease;
}

.work-lightbox img {
    max-width: min(92vw, 1200px);
    max-height: 88vh;
    border-radius: 6px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
    animation: lightbox-zoom 0.26s cubic-bezier(0.2, 0.7, 0.25, 1);
}

/* the arrows ride the viewport edges mid-height: round chips in
 * the close button's language, quiet until hovered */
.lightbox-nav {
    position: fixed;
    top: 50%;
    transform: translateY(-50%);
    width: 2.6rem;
    height: 2.6rem;
    display: grid;
    place-items: center;
    background: var(--bg-soft);
    border: 1px solid var(--line);
    border-radius: 999px;
    color: var(--ink);
    cursor: pointer;
    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
    transition: color 0.2s ease, border-color 0.2s ease;
    z-index: 1;
}

.lightbox-prev {
    left: clamp(1rem, 4vw, 3rem);
}

.lightbox-next {
    right: clamp(1rem, 4vw, 3rem);
}

.lightbox-nav:hover {
    color: var(--accent);
    border-color: var(--accent);
}

.lightbox-nav:focus-visible {
    outline: 1px solid var(--accent);
    outline-offset: 2px;
}

.lightbox-nav svg {
    width: 1rem;
    height: 1rem;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.work-lightbox::backdrop {
    background: rgba(0, 0, 0, 0.88);
}

@keyframes lightbox-in {
    from {
        opacity: 0;
    }
}

@keyframes lightbox-backdrop-in {
    from {
        opacity: 0;
    }
}

@keyframes lightbox-zoom {
    from {
        opacity: 0;
        transform: scale(0.96);
    }
}

.work-shots {
    list-style: none;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
    gap: 0.6rem;
    margin: 1.2rem 0 0;
    padding: 0;
}

.work-shot {
    position: relative;
    aspect-ratio: 16 / 10;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: 6px;
}

.work-shot img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.work-shot-num {
    position: absolute;
    left: 0.55rem;
    bottom: 0.35rem;
    font-family: var(--font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.12em;
    color: color-mix(in oklab, var(--ink) 50%, transparent);
}

.work-modal-story {
    margin: 1.1rem 0 0;
    max-width: 38rem;
    color: var(--muted);
    line-height: 1.65;
}

/* The 38rem measure above is a prose cap; the bullet form is
 * measured by the wide dialog instead (see .work-modal:has()) */
.work-modal-story:has(> .work-modal-points) {
    max-width: none;
}

.work-modal-story p {
    margin: 0;
}

/* the bullet form of a story: square markers in the accent, one
 * accomplishment per line */
.work-modal-points {
    list-style: none;
    margin: 0;
    padding: 0;
}

.work-modal-points li {
    position: relative;
    padding-left: 1rem;
    margin-bottom: 0.55rem;
}

.work-modal-points li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.55em;
    width: 5px;
    height: 5px;
    background: var(--accent);
}

/* ---------- contact dialog ---------- */

.contact-modal {
    width: min(34rem, calc(100vw - 2rem));
}

.contact-form {
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
    margin-top: 1.1rem;
}

.contact-label {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--muted);
}

.contact-input {
    font-family: var(--font-body);
    font-size: 0.95rem;
    color: var(--ink);
    background: var(--bg-soft);
    border: 0.5px solid var(--line);
    border-radius: 8px;
    padding: 0.6rem 0.75rem;
}

.contact-input:focus-visible {
    outline: 1px solid var(--accent);
    outline-offset: 1px;
}

.contact-textarea {
    resize: vertical;
    min-height: 8rem;
}

.contact-send {
    align-self: flex-start;
    font-family: var(--font-mono);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--ink);
    background: var(--bg-soft);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 0.55rem 1.4rem;
    cursor: pointer;
    transition: color 0.2s ease, border-color 0.2s ease;
}

.contact-send:hover {
    color: var(--accent);
    border-color: var(--accent);
}

.contact-send:disabled {
    opacity: 0.5;
    cursor: wait;
}

.contact-status {
    margin: 0;
    min-height: 1.2rem;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    color: var(--muted);
}

.contact-honeypot {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    opacity: 0;
}

.work-modal-stack {
    margin-top: 1.1rem;
}

.work-modal-links {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 1.25rem;
    padding: 1rem 0 0;
    margin: 1.25rem 0 0;
    border-top: 1px solid var(--line);
}

.work-modal-links a {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--ink);
    text-decoration: none;
    border-bottom: 1px solid var(--line);
    padding-bottom: 2px;
    transition: color 0.2s ease, border-color 0.2s ease;
}

.work-modal-links a:hover {
    color: var(--accent);
    border-color: var(--accent);
}

/* ---------- 404 ---------- */

.notfound {
    padding: clamp(3rem, 12vh, 7rem) 0;
}

.notfound-code {
    font-family: var(--font-display);
    font-weight: 400;
    font-size: clamp(5rem, 20vw, 12rem);
    line-height: 0.9;
    letter-spacing: -0.02em;
    margin: 0 0 1.5rem;
}

.notfound p {
    color: var(--muted);
    max-width: 30rem;
    margin: 0 0 1.5rem;
}

/* ---------- page load reveal ---------- */

.rise {
    opacity: 0;
    transform: translateY(14px);
    animation: rise 0.65s cubic-bezier(0.2, 0.7, 0.25, 1) forwards;
}

.rise-1 {
    animation-delay: 0.05s;
}

.rise-2 {
    animation-delay: 0.16s;
}

.rise-3 {
    animation-delay: 0.28s;
}

@keyframes rise {
    to {
        opacity: 1;
        transform: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .rise {
        animation: none;
        opacity: 1;
        transform: none;
    }

    .link-list a,
    .link-button {
        transition: none;
    }

    .work-card,
    .work-media::before,
    .work-badge,
    .work-close,
    .work-modal-links a {
        transition: none;
    }

    .work-modal[open],
    .work-modal[open]::backdrop,
    .work-lightbox[open],
    .work-lightbox[open]::backdrop,
    .work-lightbox img {
        animation: none;
    }

    .work-card.is-away {
        opacity: 1;
        transform: none;
    }
}
