/* ═══════════════════════════════════════════════════════════════════════════
   ryanlewan.com — "dossier" direction

   Structure comes from type, hairline rules and whitespace. There are no
   cards, panels or filled surfaces on the page: the accent is a line and a
   glow, never a flood.

   Every colour in this file resolves to a token defined on :root below. The
   legacy aliases at the end of the token block (--bg, --text, --accent, …)
   exist because /halflight, /innercity and /divergeos share this stylesheet
   and reference those names from their own inline styles.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
    /* ── ground + text ── */
    --color-bg:       #161826;
    --color-surface:  #232532;
    --color-text:     #e9e9ed;
    --color-accent:   #9184d9;
    --color-divider:  color-mix(in srgb, #e9e9ed 16%, transparent);

    /* ── neutral ramp ── */
    --color-neutral-100: #f3f5fe;
    --color-neutral-200: #e4e7f5;
    --color-neutral-300: #cfd3e5;
    --color-neutral-400: #b2b6ca;
    --color-neutral-500: #9397ab;
    --color-neutral-600: #75798c;
    --color-neutral-700: #595d6c;
    --color-neutral-800: #3f424d;
    --color-neutral-900: #292b31;

    /* ── accent ramp ── */
    --color-accent-100: #f5f4ff;
    --color-accent-200: #e7e5fe;
    --color-accent-300: #d2cefd;
    --color-accent-400: #b5abfc;
    --color-accent-500: #968ae0;
    --color-accent-600: #796cbf;
    --color-accent-700: #5d5294;
    --color-accent-800: #423a6a;
    --color-accent-900: #2b2741;

    /* The one saturated field. Reserved for deck dividers — unused on the
       page itself, kept so the system stays complete. */
    --color-section:       #262a60;
    --color-section-glow:  #353b80;
    --color-section-ghost: #4c5397;

    /* ── spacing (0.7x density — dense on purpose) ── */
    --space-1: 2.8px;
    --space-2: 5.6px;
    --space-3: 8.4px;
    --space-4: 11.2px;
    --space-6: 16.8px;
    --space-8: 22.4px;

    --radius-sm: 4px;
    --radius-md: 8px;
    --radius-lg: 14px;

    /* Elevation on a dark ground is an edge plus ambient darkness — one
       shadow, never a stack. */
    --shadow-sm: 0 0 0 1px #3f424d;
    --shadow-md: 0 0 0 1px #595d6c, 0 6px 18px rgba(0, 0, 0, 0.55);
    --shadow-lg: 0 0 0 1px #9397ab, 0 16px 40px rgba(0, 0, 0, 0.65);

    /* ── type ── */
    --font-sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
    --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

    /* ── layout ── */
    --maxw: 1400px;
    --page-x: clamp(20px, 4vw, 56px);   /* page padding, horizontal */
    --page-y: 78px;                     /* section padding, vertical */
    --gutter: 180px;                    /* left column: section number + heading */
    --gutter-gap: 56px;
    /* Where the content column starts, measured from the page edge */
    --content-x: calc(var(--page-x) + var(--gutter) + var(--gutter-gap));

    --safe-top: env(safe-area-inset-top, 0px);
    --header-h: calc(64px + var(--safe-top));

    --ease: cubic-bezier(0.2, 0.7, 0.2, 1);

    /* ── legacy aliases — consumed by the project subpages ── */
    --bg:          var(--color-bg);
    --panel:       var(--color-surface);
    --soft-1:      #1c1e2d;
    --soft-2:      #1a1c2a;
    --text:        var(--color-text);
    --muted:       var(--color-neutral-300);
    --muted-2:     var(--color-neutral-500);
    --line:        var(--color-neutral-800);
    --accent:      var(--color-accent);
    --accent-2:    var(--color-accent-300);
    --accent-glow: rgba(145, 132, 217, 0.14);
    --r-lg: var(--radius-lg);
    --pad-x: var(--page-x);
    --pad-y: var(--page-y);
}

/* ═══ Base ═══════════════════════════════════════════════════════════════ */

* { box-sizing: border-box; }

html {
    scroll-behavior: smooth;
    -webkit-text-size-adjust: 100%;
}

body {
    margin: 0;
    min-height: 100%;
    background: var(--color-bg);
    color: var(--color-text);
    font-family: var(--font-sans);
    font-size: 17px;
    font-weight: 400;
    line-height: 1.7;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* The one radial glow on the page — an ambient wash behind the hero, so the
   ground isn't a flat field. Fixed, so it never scrolls into the sections. */
body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    /* Kept off the right-hand side: the portrait dissolves into flat ground
       there, and the fade that does it is painted in --color-bg. */
    background:
        radial-gradient(820px 540px at 24% -12%, var(--color-accent-900), transparent 70%),
        radial-gradient(600px 420px at -6% 26%, #1c1e30, transparent 72%);
    opacity: 0.7;
}

h1, h2, h3, h4 {
    font-weight: 500;      /* headings never go past 500 — hierarchy is size */
    margin: 0;
}

p { margin: 0; }

img { max-width: 100%; }

/* ═══ Interaction states — no browser defaults anywhere ═══════════════════ */

a {
    color: var(--color-accent-400);
    text-decoration: none;
    transition: color 160ms var(--ease);
}
a:hover  { color: var(--color-accent-300); }
a:active { color: var(--color-accent-400); }

:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

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

:disabled, [disabled] { opacity: 0.45; }

::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
    background: var(--color-neutral-800);
    border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover { background: var(--color-neutral-700); }

/* ═══ Header ═════════════════════════════════════════════════════════════ */

.site-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 500;
    height: var(--header-h);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-8);
    padding: var(--safe-top) var(--page-x) 0;
    background: color-mix(in srgb, var(--color-bg) 82%, transparent);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
}

/* Hairline under the header, fading out at both ends like the section rules */
.site-header::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    background: linear-gradient(to right,
        transparent, var(--color-divider) var(--page-x),
        var(--color-divider) calc(100% - var(--page-x)), transparent);
}

/* Opaque strip over the iPhone status-bar inset (time/battery) — the header
   tint is translucent, so without this the page shows through up there. */
.site-header::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: var(--safe-top);
    background: var(--color-bg);
}

.header-left,
.header-right {
    display: flex;
    align-items: center;
    gap: var(--space-8);
    flex-shrink: 0;
}

.logo {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    color: var(--color-neutral-400);
    transition: color 160ms var(--ease);
}
.logo:hover { color: var(--color-accent-300); }

.nav {
    display: flex;
    align-items: center;
    gap: var(--space-8);
}
.nav a {
    position: relative;
    font-size: 14.5px;
    color: var(--color-neutral-500);
    padding: var(--space-1) 0;
}
.nav a:hover        { color: var(--color-neutral-300); }
.nav a.active       { color: var(--color-text); }
/* The active section is marked with an accent tick under the label */
.nav a.active::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -3px;
    height: 1px;
    background: var(--color-accent);
}

/* ── ">_ terminal" pill ── */
.terminal-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--color-neutral-800);
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-neutral-500);
    font-family: var(--font-mono);
    font-size: 11.5px;
    letter-spacing: 0.04em;
    line-height: 1.4;
    cursor: pointer;
    transition: border-color 160ms var(--ease), color 160ms var(--ease),
                background-color 160ms var(--ease);
}
.terminal-btn:hover {
    border-color: var(--color-accent-700);
    background: var(--color-accent-900);
    color: var(--color-accent-300);
}
.terminal-btn-glyph { color: var(--color-accent-400); }

/* ═══ Page shell ═════════════════════════════════════════════════════════ */

.scroll-container {
    position: relative;
    z-index: 1;
}

/* iOS Safari shows the slice of canvas just above the viewport behind its
   translucent status bar — a region fixed elements can never reach. This
   zero-height sticky paints a solid strip upward into it. */
.statusbar-cover {
    position: sticky;
    top: 0;
    height: 0;
    z-index: 600;
}
.statusbar-cover::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 120px;
    background: var(--color-bg);
}

/* Sections are natural-height; the class name is kept because script.js
   observes .snap-section to drive the nav + dot highlighting. */
.snap-section {
    position: relative;
    scroll-margin-top: var(--header-h);
}

/* The signature detail: a hairline between sections that fades to
   transparent at both ends, rather than a border that runs edge to edge. */
.snap-section + .snap-section::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 1px;
    background: linear-gradient(to right,
        transparent, var(--color-divider) var(--page-x),
        var(--color-divider) calc(100% - var(--page-x)), transparent);
}

/* Two columns: the mono section number + heading sit in the left margin,
   all content lives in the right column. The heading is taken out of flow so
   .section-inner's content box is exactly the content column — the bookshelf
   layout code in script.js measures against it. */
.section-inner {
    position: relative;
    max-width: var(--maxw);
    margin: 0 auto;
    padding: var(--page-y) var(--page-x) var(--page-y) var(--content-x);
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
}

.section-inner > .section-heading {
    position: absolute;
    top: var(--page-y);
    left: var(--page-x);
    width: var(--gutter);
    margin: 0;
}

.section-heading {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    align-items: flex-start;
}

.section-number {
    font-family: var(--font-mono);
    font-size: 11.5px;
    font-weight: 400;
    letter-spacing: 0.16em;
    color: var(--color-accent);
}

.section-title {
    font-size: 22px;
    font-weight: 500;
    letter-spacing: -0.01em;
    line-height: 1.2;
    color: var(--color-text);
}

/* Shared label above a sub-block: mono, uppercase, quiet */
.section-sub-heading {
    font-family: var(--font-mono);
    font-size: 11.5px;
    font-weight: 400;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-neutral-600);
    margin: 0 0 var(--space-3);
}

/* Modifier: a fading hairline above the block this label introduces */
.section-sub-heading--divided {
    padding-top: var(--space-8);
    border-top: 1px solid transparent;
    border-image: linear-gradient(to right, var(--color-divider), transparent) 1;
}

/* ═══ Buttons ════════════════════════════════════════════════════════════ */

/* Outlined, never filled. Hover fills with the darkest step of the ramp. */
.btn,
.resume-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-8);
    border: 1px solid var(--color-neutral-800);
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-neutral-500);
    font-family: var(--font-sans);
    font-size: 14.5px;
    font-weight: 400;
    line-height: 1.2;
    cursor: pointer;
    transition: border-color 160ms var(--ease), color 160ms var(--ease),
                background-color 160ms var(--ease), box-shadow 200ms var(--ease);
}
.btn:hover {
    border-color: var(--color-neutral-600);
    color: var(--color-neutral-300);
}
.btn:active { color: var(--color-neutral-400); }

/* Accent-outlined. The résumé link is the primary action of its section, so
   it carries the same weight as "See my works" in the hero. */
.btn--primary,
.resume-btn {
    border-color: var(--color-accent);
    color: var(--color-accent-300);
}
.btn--primary:hover,
.resume-btn:hover {
    background: var(--color-accent-900);
    border-color: var(--color-accent);
    color: var(--color-accent-200);
    /* the accent as a glow, not a flood */
    box-shadow: 0 0 0 1px var(--color-accent-800), 0 4px 18px rgba(145, 132, 217, 0.16);
}
.btn--primary:active,
.resume-btn:active { color: var(--color-accent-400); }

.resume-btn svg { flex-shrink: 0; }

/* ═══ 01 · Hero ══════════════════════════════════════════════════════════ */

#hero .section-inner {
    padding: calc(var(--header-h) + 84px) var(--page-x) var(--page-y);
    gap: 0;
}

/* Mono kicker line above the name */
.meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    align-items: baseline;
    font-family: var(--font-mono);
    font-size: 11.5px;
    font-weight: 400;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-neutral-600);
}
/* Separators are drawn, not typed, so the copy stays as authored */
.meta span + span::before {
    content: "· ";
    color: var(--color-neutral-700);
}
#hero .meta::before {
    content: "01 /";
    color: var(--color-accent);
}

.hero {
    display: grid;
    grid-template-columns: 1fr 340px;
    gap: var(--gutter-gap);
    align-items: end;
    margin-top: var(--space-8);
}

h1 {
    font-size: clamp(48px, 8.2vw, 104px);
    font-weight: 500;
    line-height: 0.92;
    letter-spacing: -0.045em;
    color: var(--color-text);
    /* Two lines, flush left — the wrap point is the layout, not a <br> */
    max-width: 8ch;
    text-wrap: balance;
}

.sub {
    margin-top: var(--space-8);
    max-width: 520px;
    font-size: 20px;
    font-weight: 400;
    line-height: 1.5;
    color: var(--color-neutral-400);
    text-wrap: pretty;
}

.btn-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-4);
    margin-top: 28px;
}

/* Social row — mono, quiet, no separators drawn by CSS (the "/" is markup) */
.links {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-4);
    margin-top: 28px;
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.04em;
}
.links a,
.links button { color: var(--color-neutral-600); }
.links a:hover,
.links button:hover { color: var(--color-accent-300); }
.links > span { color: var(--color-neutral-800); }

.copy-email {
    -webkit-appearance: none;
    appearance: none;
    padding: 0;
    margin: 0;
    border: 0;
    background: none;
    font: inherit;
    color: var(--color-neutral-600);
    cursor: pointer;
    transition: color 160ms var(--ease);
}
.copy-email.copied { color: var(--color-accent-300); }

/* ── Portrait ──
   The brief calls for mix-blend-mode: lighten so the photo sits in the page
   rather than on it. That only works on a photo shot against a dark backdrop;
   this headshot has a light-grey one, which blending would turn into a bright
   slab. Same intent, different mechanism: desaturate and dim the image, then
   vignette its edges into --color-bg so it dissolves into the ground. The
   fade is painted in the background colour, which is why the hero glow above
   is kept away from this corner. */
/* Shrink-wraps the portrait so ::after lines up with it exactly. The image
   below must keep an explicit px width for this: a percentage width inside a
   shrink-to-fit box resolves against the image's intrinsic size (971px) and
   blows the page out sideways on a phone. */
.visual {
    position: relative;
    width: max-content;
    max-width: 100%;
    margin-left: auto;
    line-height: 0;
}

.visual::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background:
        linear-gradient(to bottom, transparent 30%, var(--color-bg) 94%),
        linear-gradient(to top,    transparent 78%, var(--color-bg) 100%),
        linear-gradient(to right,  var(--color-bg), transparent 34%),
        linear-gradient(to left,   var(--color-bg), transparent 34%);
}

.profile-circle {
    display: block;
    width: 340px;
    height: 420px;
    object-fit: cover;
    object-position: center 16%;
    filter: grayscale(0.72) contrast(1.06) brightness(0.74);
}

/* ═══ 02 · About ═════════════════════════════════════════════════════════ */

.about-grid {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
}

.about-text {
    max-width: 640px;
    font-size: 17px;
    line-height: 1.7;
    color: var(--color-neutral-300);
    text-wrap: pretty;
}

/* Three labelled blocks in a row: education / currently learning / status */
.about-meta {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--gutter-gap);
    margin-top: var(--space-8);
    max-width: 900px;
}

.about-col { min-width: 0; }

.about-education {
    font-size: 14.5px;
    line-height: 1.6;
    color: var(--color-neutral-300);
}
.edu-gpa {
    display: inline-block;
    margin-top: var(--space-2);
    font-family: var(--font-mono);
    font-size: 11.5px;
    font-weight: 700;      /* loaded alongside 400/500 in the font link */
    letter-spacing: 0.04em;
    color: var(--color-accent-300);
}
.about-education br + span { display: inline-block; }

.learning-list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    font-size: 14.5px;
    line-height: 1.6;
    color: var(--color-neutral-300);
}

.about-availability {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-3);
    font-size: 14.5px;
    line-height: 1.6;
    color: var(--color-neutral-300);
}

.availability-dot {
    flex-shrink: 0;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-accent-400);
    box-shadow: 0 0 8px var(--color-accent-500);
    animation: pulse-dot 2.4s ease-in-out infinite;
}
@keyframes pulse-dot {
    0%, 100% { opacity: 1;   }
    50%      { opacity: 0.35; }
}

/* ── Skill chips ── */
.skill-grid {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.skill,
.tech-tag {
    display: inline-flex;
    align-items: center;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-neutral-800);
    border-radius: var(--radius-sm);
    background: transparent;
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.04em;
    line-height: 1.35;
    color: var(--color-neutral-400);
    transition: border-color 160ms var(--ease), color 160ms var(--ease);
}
.skill:hover {
    border-color: var(--color-accent-700);
    color: var(--color-accent-300);
}

.about-grid > .resume-btn { align-self: flex-start; }

/* ═══ 03 · Projects ══════════════════════════════════════════════════════ */

/* Arrows sit at the far right, opposite the section number. Kept in flow
   rather than absolute: .project-list is positioned (script.js needs it as
   the offsetParent), so an absolute sibling earlier in the DOM would be
   painted underneath the track. In flow it also reserves its own row. */
.carousel-controls {
    display: flex;
    gap: var(--space-2);
    align-self: flex-end;
}

.carousel-arrow {
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid var(--color-neutral-800);
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-neutral-500);
    font-family: var(--font-mono);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    transition: border-color 160ms var(--ease), color 160ms var(--ease),
                background-color 160ms var(--ease), opacity 160ms var(--ease);
}
.carousel-arrow:hover:not(:disabled) {
    border-color: var(--color-accent-700);
    background: var(--color-accent-900);
    color: var(--color-accent-300);
}
.carousel-arrow:disabled { opacity: 0.45; cursor: default; }

/* ── The track ──
   position: relative is load-bearing: script.js compares each card's
   offsetLeft against this element's own scrollLeft, so the track has to be
   the offsetParent. scroll-padding-left is read by the same code as the
   "peek" inset — with a flush-left track it is 0. */
.project-list {
    position: relative;
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    gap: var(--gutter-gap);
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x proximity;
    scroll-padding-left: 0;
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.project-list::-webkit-scrollbar { display: none; }
.project-list:focus-visible { outline: none; }

.project-list > li {
    list-style: none;
    flex: 0 0 400px;
    max-width: 400px;
    display: flex;
    scroll-snap-align: start;
}

/* ── A card that isn't a card ──
   No container, no fill, no border around the whole thing: an image, then
   type. The only edge is the 1px line around the screenshot. */
.project {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
    color: var(--color-text);
}

.project-media {
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border: 1px solid var(--color-neutral-900);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    transition: border-color 200ms var(--ease);
}
.project-media img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: var(--media-pos, center);
    filter: grayscale(0.25) brightness(0.9);
    transition: filter 260ms var(--ease), transform 420ms var(--ease);
}
.project:hover .project-media          { border-color: var(--color-accent-800); }
.project:hover .project-media img      { filter: none; transform: scale(1.015); }

.project-body {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
    padding-top: var(--space-6);
}

.project-top {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
}

.project-title-row {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-3);
    min-width: 0;
}

.project strong {
    font-size: 18px;
    font-weight: 500;
    letter-spacing: -0.005em;
    color: var(--color-text);
}

/* Status is the machine voice — mono, lowercase-ish, no pill, no fill */
.project-status {
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 400;
    letter-spacing: 0.04em;
    color: var(--color-accent-400);
    white-space: nowrap;
}

.arrow {
    flex-shrink: 0;
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--color-neutral-700);
    transition: color 160ms var(--ease), transform 200ms var(--ease);
}
.project:hover .arrow {
    color: var(--color-accent-400);
    transform: translate(2px, -2px);
}

.project .desc {
    margin-top: var(--space-3);
    font-size: 14.5px;
    line-height: 1.6;
    color: var(--color-neutral-500);
    text-wrap: pretty;
}

.project-bullets {
    margin: var(--space-4) 0 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
.project-bullets li {
    position: relative;
    padding-left: var(--space-6);
    font-size: 13.5px;
    line-height: 1.55;
    color: var(--color-neutral-600);
}
.project-bullets li::before {
    content: "—";
    position: absolute;
    left: 0;
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--color-neutral-800);
}
.project-bullets code {
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--color-accent-400);
}

/* The stack line under the card — one mono run, not chips */
.project-tags {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2);
    margin-top: auto;
    padding-top: var(--space-6);
    font-family: var(--font-mono);
    font-size: 11.5px;
    letter-spacing: 0.04em;
    color: var(--color-neutral-600);
}
.project .tech-tag {
    padding: 0;
    border: 0;
    border-radius: 0;
    font-size: inherit;
    letter-spacing: inherit;
    color: inherit;
}
.project .tech-tag + .tech-tag::before {
    content: "· ";
    color: var(--color-neutral-800);
}

/* ── Position counter ──
   The dots are relabelled as a mono "01 / 05" counter; script.js still
   toggles .active on each dot, and CSS turns the active one into the
   left-hand number while the rest collapse to a hairline tick. */
.carousel-dots {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-6);
}
.carousel-dot {
    width: 18px;
    height: 1px;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: var(--color-neutral-800);
    cursor: pointer;
    transition: background-color 200ms var(--ease), width 320ms var(--ease);
}
.carousel-dot:hover  { background: var(--color-neutral-600); }
.carousel-dot.active { width: 30px; background: var(--color-accent); }

/* "See all projects ↗" — a mono link under the track, not a button */
.see-all-btn {
    align-self: flex-start;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    font-family: var(--font-mono);
    font-size: 11.5px;
    letter-spacing: 0.04em;
    color: var(--color-neutral-500);
}
.see-all-btn:hover {
    background: none;
    border: 0;
    color: var(--color-accent-300);
}

/* ═══ 04 · Reading ═══════════════════════════════════════════════════════ */

.reading-intro {
    max-width: 640px;
    font-size: 17px;
    line-height: 1.7;
    color: var(--color-neutral-300);
    text-wrap: pretty;
}
.reading-cta { color: var(--color-neutral-500); }

/* ── The shelf ──
   No case, no frame: spines standing on a hairline. script.js reads
   --book-scale and the row's column-gap to decide how many spines fit a row,
   and measures the available width from .section-inner's content box. */
.bookshelf {
    --book-scale: 0.85;
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
    /* Headroom so a hovered book's info card clears the fixed header */
    margin-top: clamp(32px, 6vh, 64px);
    width: 100%;
    /* Pulls the centred row (and the rule under it, so the two stay aligned)
       left of the content column's true centre. script.js subtracts this
       padding when working out how many spines fit a row. */
    padding-right: clamp(0px, 9vw, 130px);
}

.shelf {
    display: flex;
    flex-direction: column;
}

/* Bottom-aligned so mixed spine heights sit flush on the shelf line */
.shelf-books {
    display: flex;
    align-items: flex-end;
    justify-content: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    min-height: 150px;
    padding: 0;
}

/* The shelf itself is a rule. Symmetric now that the spines are centred —
   the same fade-at-both-ends language as the section dividers. */
.shelf-board {
    height: 1px;
    background: linear-gradient(to right,
        transparent, var(--color-divider) 18%,
        var(--color-neutral-600) 50%,
        var(--color-divider) 82%, transparent);
}

.shelf-hint {
    margin: auto 0;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-neutral-700);
}

/* ── A single spine ── */
.book {
    position: relative;
    flex: 0 0 auto;
    width: calc(var(--book-w, 42px) * var(--book-scale, 1));
    height: calc(var(--book-h, 170px) * var(--book-scale, 1));
    padding: 0;
    border: 1px solid var(--color-neutral-800);
    border-radius: var(--radius-sm);
    background: var(--color-neutral-900);
    color: var(--color-neutral-400);
    font-family: var(--font-mono);
    cursor: pointer;
    /* No overflow:hidden here — .book-info is positioned outside the spine and
       would be clipped away. The cover art is a background, so border-radius
       already rounds it. */
    transform-origin: 50% 100%;
    transition: transform 240ms var(--ease), border-color 200ms var(--ease),
                background-color 200ms var(--ease);
}
.book:hover,
.book:focus-visible {
    transform: translateY(-14px);
    border-color: var(--color-accent-800);
    background: var(--color-accent-900);
    z-index: 5;
}

/* Cover artwork, when a book defines one. Desaturated and dimmed so the
   shelf reads as one material inside the palette; full colour on hover. */
.book--art {
    background:
        var(--book-image) var(--book-image-pos, center) / cover no-repeat,
        var(--color-neutral-900);
    filter: grayscale(0.82) brightness(0.55) contrast(1.02);
    transition: transform 240ms var(--ease), border-color 200ms var(--ease),
                filter 260ms var(--ease);
}
.book--art:hover,
.book--art:focus-visible {
    background:
        var(--book-image) var(--book-image-pos, center) / cover no-repeat,
        var(--color-neutral-900);
    filter: grayscale(0.1) brightness(0.98);
}
/* The artwork carries the visible title; the text stays for screen readers */
.book--art .book-spine-title { opacity: 0; }

.book-spine-title {
    position: absolute;
    inset: var(--space-3) 0;
    margin: auto;
    writing-mode: vertical-rl;
    font-family: var(--font-mono);
    font-size: 10.5px;
    font-weight: 400;
    letter-spacing: 0.04em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ── "What I learned", revealed above the pulled spine ── */
.book-info {
    position: absolute;
    /* Anchored to the spine's left edge (not centred) so cards on books near
       the shelf's left end never clip off the viewport. --card-dx is set
       inline by script.js to nudge a clipping card back inward. */
    left: calc(-14px + var(--card-dx, 0px));
    bottom: calc(100% + var(--space-6));
    transform: translateY(6px);
    width: min(320px, 74vw);
    padding: var(--space-4) var(--space-6);
    background: rgba(13, 14, 22, 0.97);
    border: 1px solid var(--color-accent-800);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
    text-align: left;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    /* the spine may be filtered/greyed — the card must not inherit that */
    filter: none;
    transition: opacity 180ms var(--ease), transform 180ms var(--ease), visibility 180ms;
    z-index: 10;
}
.book:hover .book-info,
.book:focus-visible .book-info {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}
.book-info::after {
    content: "";
    position: absolute;
    top: 100%;
    left: calc(14px + var(--book-w, 42px) * var(--book-scale, 1) / 2 - 6px - var(--card-dx, 0px));
    border: 6px solid transparent;
    border-top-color: var(--color-accent-800);
}

.book-info-title {
    display: block;
    font-family: var(--font-sans);
    font-size: 14.5px;
    font-weight: 500;
    line-height: 1.4;
    letter-spacing: normal;
    color: var(--color-text);
}
.book-info-author {
    display: block;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
    color: var(--color-neutral-600);
}
.book-info-label {
    display: block;
    margin: var(--space-3) 0 var(--space-2);
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 400;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-accent-400);
}
.book-info-text {
    display: block;
    font-family: var(--font-sans);
    font-size: 13.5px;
    font-weight: 400;
    line-height: 1.6;
    letter-spacing: normal;
    text-transform: none;
    color: var(--color-neutral-500);
    /* pre-line so a \n in a book's `learned` starts a new takeaway line */
    white-space: pre-line;
    writing-mode: horizontal-tb;
}
.book-info-point {
    font-weight: 500;
    color: var(--color-neutral-300);
}

/* ═══ Footer ═════════════════════════════════════════════════════════════ */

.site-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-6);
    padding-top: var(--space-8);
    font-family: var(--font-mono);
    font-size: 11.5px;
    letter-spacing: 0.04em;
    color: var(--color-neutral-600);
}

/* Pull the footer out of the content column so it spans the full page width,
   and give it its own fading rule. */
.section-inner > .site-footer {
    margin-top: 78px;
    margin-left: calc(-1 * (var(--gutter) + var(--gutter-gap)));
    border-top: 1px solid transparent;
    border-image: linear-gradient(to right,
        var(--color-divider), var(--color-divider) 70%, transparent) 1;
}

.site-footer .links {
    margin-top: 0;
    font-size: 11.5px;
}

/* ═══ Side navigation dots ═══════════════════════════════════════════════ */

.scroll-dots {
    position: fixed;
    right: 22px;
    top: 50%;
    transform: translateY(-50%);
    z-index: 400;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: var(--space-4);
}

.scroll-dots .dot {
    position: relative;
    width: 14px;
    height: 1px;
    background: var(--color-neutral-800);
    transition: width 260ms var(--ease), background-color 200ms var(--ease);
}
.scroll-dots .dot:hover  { width: 22px; background: var(--color-neutral-500); }
.scroll-dots .dot.active { width: 26px; background: var(--color-accent); }

/* Label that slides in on hover */
.scroll-dots .dot::after {
    content: attr(data-label);
    position: absolute;
    right: calc(100% + var(--space-3));
    top: 50%;
    transform: translateY(-50%);
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-neutral-600);
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity 180ms var(--ease);
}
.scroll-dots .dot:hover::after { opacity: 1; }

/* ═══ "Currently building" toast ═════════════════════════════════════════ */

.work-toast {
    position: fixed;
    right: var(--page-x);
    bottom: var(--space-8);
    z-index: 450;
    width: min(320px, calc(100vw - 2 * var(--page-x)));
    border: 1px solid var(--color-neutral-800);
    border-radius: var(--radius-md);
    background: rgba(13, 14, 22, 0.96);
    box-shadow: var(--shadow-md);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    overflow: hidden;
    animation: toast-in 500ms var(--ease) 600ms both;
}
@keyframes toast-in {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: translateY(0); }
}

.work-toast.leaving {
    opacity: 0;
    transform: translateY(12px);
    transition: opacity 300ms var(--ease), transform 300ms var(--ease);
}
.work-toast.dismissed { display: none; }

.work-toast-link {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-4);
    color: inherit;
}

.work-toast-logo {
    width: 42px;
    height: 42px;
    flex-shrink: 0;
    object-fit: cover;
    border: 1px solid var(--color-neutral-900);
    border-radius: var(--radius-sm);
    filter: grayscale(0.2);
}

.work-toast-body {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}

.work-toast-eyebrow {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-family: var(--font-mono);
    font-size: 10.5px;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-neutral-600);
}
.work-toast-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--color-accent-400);
    animation: pulse-dot 2.4s ease-in-out infinite;
}

.work-toast-title {
    font-size: 14.5px;
    font-weight: 500;
    color: var(--color-text);
}
.work-toast-text {
    font-size: 12.5px;
    line-height: 1.4;
    color: var(--color-neutral-500);
}
.work-toast-arrow { color: var(--color-accent-400); }

.work-toast-close {
    position: absolute;
    top: var(--space-1);
    right: var(--space-2);
    padding: 0 var(--space-2);
    border: 0;
    background: none;
    color: var(--color-neutral-700);
    font-size: 15px;
    line-height: 1.4;
    cursor: pointer;
    transition: color 160ms var(--ease);
}
.work-toast-close:hover { color: var(--color-neutral-300); }

/* Countdown along the bottom edge — drains over the toast's 3s lifetime */
.work-toast-progress {
    position: absolute;
    left: 0;
    bottom: 0;
    height: 1px;
    width: 100%;
    background: var(--color-accent-700);
    transform-origin: left;
    animation: toast-drain 3s linear 1100ms both;
}
@keyframes toast-drain {
    from { transform: scaleX(1); }
    to   { transform: scaleX(0); }
}

/* ═══ Terminal (ryanOS) ══════════════════════════════════════════════════ */

/* Opens as a strip docked to the bottom of the page, inset from both sides.
   The overlay itself stays full-bleed so a click outside still closes it
   (script.js checks e.target === overlay) and so the visualViewport fitting
   on phones has something to size. */
.terminal-overlay {
    position: fixed;
    inset: 0;
    z-index: 900;
    display: flex;
    align-items: flex-end;
    padding: 0 var(--page-x) var(--space-8);
    background: rgba(13, 14, 22, 0.55);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
}
.terminal-overlay[hidden] { display: none; }

.terminal-window {
    width: 100%;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--color-accent-800);
    border-radius: var(--radius-md);
    background: rgba(13, 14, 22, 0.96);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
}

.terminal-titlebar {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-4);
    border-bottom: 1px solid var(--color-neutral-900);
}
.terminal-title {
    flex: 1;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
    color: var(--color-neutral-600);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.terminal-close {
    padding: 0 var(--space-2);
    border: 0;
    background: none;
    color: var(--color-neutral-600);
    font-size: 15px;
    line-height: 1.4;
    cursor: pointer;
    transition: color 160ms var(--ease);
}
.terminal-close:hover { color: var(--color-neutral-300); }

.terminal-body {
    flex: 1;
    /* Taller than the brief's 168px — enough room to actually read the output
       of `ls`, `tree` or `cat` without scrolling. min-height gives it a real
       terminal shape the moment it opens (the prompt pins to the bottom, so
       it fills downward); max-height caps how far it grows with output. */
    min-height: min(320px, 42vh);
    max-height: min(460px, 54vh);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    padding: var(--space-4) var(--space-6);
    font-family: var(--font-mono);
    font-size: 12.5px;
    line-height: 1.6;
    color: var(--color-neutral-300);
    cursor: text;
}

.terminal-motd     { margin: 0 0 var(--space-3); color: var(--color-neutral-600); }
.terminal-motd-cmd { color: var(--color-accent-400); }
.terminal-error    { color: #d98a7e; }
.terminal-featured { color: var(--color-accent-300); }
.terminal-entry-dir{ color: var(--color-accent-400); }
.terminal-link     { color: var(--color-accent-400); text-decoration: underline; }
.terminal-link:hover { color: var(--color-accent-300); }
.terminal-grep-file  { color: var(--color-neutral-600); margin-right: var(--space-3); }
.terminal-grep-match { color: var(--color-bg); background: var(--color-accent-400); border-radius: 2px; }

.terminal-history .terminal-line { margin: 0; }
.terminal-line {
    display: block;
    word-break: break-all;
    white-space: pre-wrap;
}
/* Active prompt line pins to the bottom; history stacks above it */
.terminal-body > .terminal-line { margin-top: auto; }
.terminal-prompt { color: var(--color-accent-400); margin-right: var(--space-3); }
.terminal-typed  { color: var(--color-neutral-300); }

.terminal-cursor {
    display: inline-block;
    width: 7px;
    height: 15px;
    margin-left: 1px;
    vertical-align: text-bottom;
    background: var(--color-accent);
    animation: terminal-blink 1.1s steps(1) infinite;
}
@keyframes terminal-blink { 50% { opacity: 0; } }

/* Real keystrokes land here, mirrored into .terminal-typed so the block
   cursor can sit at the end. Must stay ≥16px and on-screen: iOS zooms the
   page when focusing a smaller input. */
.terminal-hidden-input {
    position: absolute;
    left: 0;
    bottom: 0;
    width: 1px;
    height: 1px;
    font-size: 16px;
    opacity: 0;
    pointer-events: none;
}

body.terminal-open { overflow: hidden; }

/* ═══ Scroll reveal ══════════════════════════════════════════════════════
   .reveal is added by script.js, so content is never hidden if JS is off. */

.reveal {
    opacity: 0;
    transform: translateY(10px);
    transition: opacity 600ms var(--ease), transform 600ms var(--ease);
}
.reveal.revealed {
    opacity: 1;
    transform: none;
}

/* ═══ Responsive ═════════════════════════════════════════════════════════ */

/* Tablet — the left gutter is too expensive below this, so the section
   number and heading move inline above the content. */
@media (max-width: 900px) {
    :root { --page-y: 56px; }

    .section-inner {
        padding-left: var(--page-x);
        padding-right: var(--page-x);
    }
    .section-inner > .section-heading {
        position: static;
        width: auto;
        flex-direction: row;
        align-items: baseline;
        gap: var(--space-4);
    }
    .section-inner > .site-footer { margin-left: 0; }

    .hero {
        grid-template-columns: 1fr;
        gap: var(--space-8);
        align-items: start;
    }
    .visual { margin-left: 0; }
    .profile-circle { width: 280px; height: 340px; }

    h1 { max-width: none; }

    .about-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-8); }

    .scroll-dots { display: none; }
}

/* Phone */
@media (max-width: 600px) {
    body { font-size: 16px; }

    :root { --page-y: 44px; --header-h: calc(56px + var(--safe-top)); }

    .site-header { gap: var(--space-3); }
    .nav { gap: var(--space-4); }
    .nav a { font-size: 13px; }
    .terminal-btn-label { display: none; }   /* just the >_ glyph */

    #hero .section-inner { padding-top: calc(var(--header-h) + 44px); }
    h1 { letter-spacing: -0.035em; }
    .sub { font-size: 17px; }
    .profile-circle { width: 260px; height: 300px; }

    .about-text, .reading-intro { font-size: 16px; }
    .about-meta { grid-template-columns: 1fr; gap: var(--space-8); }

    .project-list { gap: var(--space-8); }
    .project-list > li { flex-basis: min(400px, 82vw); max-width: min(400px, 82vw); }

    /* Shrink spines via the scale factor — each book's inline --book-w/--book-h
       would override a plain .book re-declaration here. script.js reads this
       to decide how many books fit per shelf row. */
    .bookshelf   { --book-scale: 0.68; margin-top: 72px; padding-right: 0; }
    .shelf-books { min-height: 120px; }

    .work-toast { width: calc(100vw - 2 * var(--page-x)); }

    /* Fill the visible area — script.js keeps the overlay above the on-screen
       keyboard via visualViewport — instead of a floating strip */
    .terminal-overlay { padding: var(--space-2); align-items: stretch; }
    .terminal-window  { height: 100%; }
    .terminal-body    { min-height: 0; max-height: none; padding: var(--space-3) var(--space-4); }
}

/* ═══ Reduced motion ═════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
    }
    html, .project-list { scroll-behavior: auto !important; }
    .reveal { opacity: 1; transform: none; }
}
