/* ---- tokens ---- */
:root {
  --bg: #1b1b1b;
  --surface: #232323;
  --border: #343434;
  --text: #e8e8e8;
  --text-muted: #9a9a9a;
  --accent: #9b3654;
  /* fills, rules, borders only. 2.5:1 on --bg, never text */
  --accent-text: #db6387;
  /* text and links. 5.04:1 on --bg, 4.6:1 on --surface */
  /* Paper. Documents on this site render light: the resume sheet and the
     contact card. The tint carries a faint rose cast pulled from the crimson
     so it reads as part of this palette rather than generic cream.
     Measured on --paper: --ink 14.04:1, --ink-soft 6.20:1, --accent 5.91:1. */
  --paper: #f2ecee;
  --paper-edge: #d8cbd0;
  --ink: #241e21;
  --ink-soft: #5f545a;

  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --maxw: 62rem;
  --gap: 1.5rem;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

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

/* NOTE: the old stylesheet set `overflow: hidden` here, which prevented the
   document from scrolling at all. Do not reintroduce it. */

a {
  color: var(--accent-text);
}

/* h4-h6 included: without them a card title promoted to h4 kept the browser
   default 1.33em margin and opened a gap above its description. */
h1,
h2,
h3,
h4,
h5,
h6 {
  line-height: 1.2;
  margin: 0;
}

img {
  max-width: 100%;
  height: auto;
}

.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 1.25rem;
}

.section {
  padding: 4.5rem 0;
  border-top: 1px solid var(--border);
}

.section:first-of-type {
  border-top: 0;
}

.section-title {
  font-size: 0.8rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 2rem;
}

.section-title::after {
  content: "";
  display: block;
  width: 2.5rem;
  height: 2px;
  background: var(--accent);
  margin-top: 0.6rem;
}

/* ---- skip link ---- */
.skip-link {
  position: absolute;
  left: -9999px;
}

.skip-link:focus {
  left: 1rem;
  top: 1rem;
  z-index: 200;
  background: var(--surface);
  color: var(--text);
  padding: 0.6rem 1rem;
  border: 1px solid var(--accent);
}

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

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* ---- a11y helper ---- */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- nav ---- */
.nav {
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(27, 27, 27, 0.92);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}

.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 3.5rem;
}

.nav-brand {
  font-weight: 700;
  letter-spacing: 0.15em;
  color: var(--text);
  text-decoration: none;
}

.nav-links {
  display: flex;
  gap: 0.35rem;
}

.nav-links a {
  color: var(--text);
  text-decoration: none;
  padding: 0.5rem 0.85rem;
  font-size: 0.9rem;
  border-radius: 3px;
}

.nav-links a:hover,
.nav-links a:focus-visible {
  color: var(--accent-text);
  background: var(--surface);
}

.nav-toggle {
  display: none;
  background: none;
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0.5rem 0.6rem;
  cursor: pointer;
}

.nav-toggle-bar {
  display: block;
  width: 20px;
  height: 2px;
  background: var(--text);
  margin: 4px 0;
}

@media (max-width: 640px) {
  .nav-inner {
    flex-wrap: wrap;
  }

  .nav-toggle {
    display: block;
  }

  .nav-links {
    display: none;
    flex-direction: column;
    width: 100%;
    padding-bottom: 0.75rem;
    gap: 0;
  }

  .nav-links.is-open {
    display: flex;
  }

  .nav-links a {
    padding: 0.75rem 0;
  }
}

/* ---- short pages ----
   A page whose content is much shorter than the viewport (contact) otherwise
   sits top-aligned above a screen of empty dark, which reads emptier than the
   page it replaced. Centring the block composes it instead. Uses min-height,
   so longer content still scrolls normally. */
.page-short {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.page-short main {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.page-short .page-head {
  padding-top: 0;
}

.page-short .section {
  padding: 2rem 0 0;
  border-top: 0;
}

/* ---- landing: one viewport, no scroll ----
   min-height rather than height, so a short viewport (landscape phone) grows
   and scrolls instead of clipping the buttons off the bottom. */
.landing {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.landing .hero {
  flex: 1 1 auto;
  min-height: 0;
  padding: 2rem 0 3rem;
}

/* ---- hero ---- */
/* The canvas is a decorative background layer. `.hero::after` lays a vignette
   of the page background over it so the headline keeps its contrast ratio no
   matter where the points happen to settle. */
.hero {
  position: relative;
  overflow: hidden;
  min-height: 80vh;
  display: flex;
  align-items: center;
  padding: 5rem 0 4.5rem;
}

.hero-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
  pointer-events: none;
  opacity: 0.9;
  /* The mask, not an overlay, is what protects text contrast: the middle band
     is fully transparent, so no point is ever drawn behind the headline and
     the text always sits on flat --bg. Measured without it, a dot behind the
     tagline gave 3.99:1, under the 4.5:1 floor. Above and below that band the
     field renders undimmed. */
  -webkit-mask-image: linear-gradient(
    to bottom,
    #000 0%,
    #000 18%,
    transparent 28%,
    transparent 72%,
    #000 82%,
    #000 100%
  );
  mask-image: linear-gradient(
    to bottom,
    #000 0%,
    #000 18%,
    transparent 28%,
    transparent 72%,
    #000 82%,
    #000 100%
  );
}

/* Fades the field into the page edges so it does not stop on a hard line. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(
      to bottom,
      var(--bg) 0%,
      rgba(27, 27, 27, 0) 10%,
      rgba(27, 27, 27, 0) 90%,
      var(--bg) 100%
    ),
    linear-gradient(
      to right,
      var(--bg) 0%,
      rgba(27, 27, 27, 0) 7%,
      rgba(27, 27, 27, 0) 93%,
      var(--bg) 100%
    );
}

.hero-inner {
  position: relative;
  z-index: 2;
  text-align: center;
}

/* Contrast guarantee, anchored to the content rather than to a fixed fraction
   of the hero. The band mask above is sized for a typical viewport, but the
   text block grows relative to the hero on short, wide windows and would push
   past the clear band. This scrim moves with the text, so whatever the
   viewport, the headline always sits on flat --bg. */
.hero-inner::before {
  content: "";
  position: absolute;
  /* A rectangle, not an ellipse: the text block is wide and short, and an
     inscribed ellipse leaves its corners uncovered (measured at 1.17x the
     ellipse radius). The inset exceeds the blur radius, so every text edge
     sits inside the fully opaque core and only the outer falloff is soft. */
  inset: -4rem -3rem;
  z-index: -1;
  pointer-events: none;
  background: var(--bg);
  filter: blur(24px);
}

.hero-name {
  font-size: clamp(2rem, 7vw, 3.25rem);
  color: var(--text);
  letter-spacing: -0.02em;
}

.hero-role {
  color: var(--accent-text);
  font-size: 1rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  margin: 0.6rem 0 1.25rem;
}

.hero-tagline {
  max-width: 42rem;
  margin: 0 auto;
  color: var(--text-muted);
  font-size: 1.02rem;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  justify-content: center;
  margin-top: 2rem;
}

.btn {
  display: inline-block;
  padding: 0.7rem 1.4rem;
  border: 1px solid var(--border);
  border-radius: 3px;
  color: var(--text);
  text-decoration: none;
  font-size: 0.92rem;
}

.btn:hover,
.btn:focus-visible {
  border-color: var(--accent-text);
  color: var(--accent-text);
}

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.btn-primary:hover,
.btn-primary:focus-visible {
  background: #b0405f;
  border-color: #b0405f;
  color: #fff;
}

/* ---- anchor offset ---- */
/* The nav is position:sticky at ~57px. `.section`'s own 4.5rem top padding
   already clears it by ~15px, so this is not a fix for a hidden heading. It
   raises the gap to ~87px so a jumped-to heading reads as a heading rather
   than sitting flush under the nav, and keeps the anchor correct if that
   padding ever changes. */
.section {
  scroll-margin-top: 4.5rem;
}

/* ---- cards ---- */
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  gap: var(--gap);
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1.5rem;
  display: flex;
  flex-direction: column;
}

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

.card-title {
  font-size: 1.15rem;
  color: var(--text);
}

.card-desc {
  color: var(--text-muted);
  font-size: 0.95rem;
  margin: 0.75rem 0 1rem;
}

.card-tags {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0;
  margin: 0 0 1.25rem;
}

.card-tags li {
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: 2px;
  padding: 0.2rem 0.5rem;
}

.card-links {
  margin: auto 0 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

.card-live {
  font-size: 0.92rem;
  text-decoration: none;
}

.card-live:hover {
  text-decoration: underline;
}

.card-live::after {
  content: " \2192";
}

/* The right-hand slot when the repo is actually public. Muted like
   .card-private so the two read as the same kind of note, but it is a link. */
.card-src {
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  text-decoration: none;
  border-bottom: 1px solid var(--border);
}

.card-src:hover,
.card-src:focus-visible {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
}

.card-private {
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ---- about ---- */
.about p {
  max-width: 44rem;
  color: var(--text-muted);
}

.about p + p {
  margin-top: 1rem;
}

.about-more {
  margin-top: 2rem;
}

/* ---- contact card ----
   An actual business card: 3.5x2 landscape proportions, monogram, identity
   block, details set along the bottom. Named .vcard rather than .card so it
   cannot collide with the project cards, which are dark. */
.vcard {
  max-width: 30rem;
  aspect-ratio: 1.75 / 1;
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  background: var(--paper);
  color: var(--ink);
  border-radius: 4px;
  padding: 1.6rem 1.9rem 1.6rem 2.1rem;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.6),
    0 1px 2px rgba(0, 0, 0, 0.4),
    0 18px 40px rgba(0, 0, 0, 0.4);
}

/* Spot colour bled off the left edge, the way a printed card carries its ink. */
.vcard::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 6px;
  background: var(--accent);
}

.vcard-mono {
  margin: 0 0 0.9rem;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.32em;
  color: var(--accent);
}

/* Identity left, details right, both sitting on the card's baseline. Stacked
   left-aligned they left the right half of the card empty. */
.vcard-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.25rem 1.5rem;
}

.vcard-name {
  margin: 0;
  font-size: 1.25rem;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.vcard-role {
  margin: 0.25rem 0 0;
  color: var(--ink-soft);
  font-size: 0.68rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
}

.vcard-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 0.78rem;
  line-height: 1.8;
  color: var(--ink-soft);
  text-align: right;
}

/* No field labels: a business card does not caption its own phone number, and
   each value already says what it is. The underline is what still marks the
   links as links without them. */
.vcard-list a {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--paper-edge);
}

.vcard-list a:hover,
.vcard-list a:focus-visible {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* Below the card's own width the fixed ratio would squash the text, and the
   two columns get too tight to read, so the card grows tall and stacks.
   This block sits after the rules it overrides: at equal specificity source
   order decides, and placed above them it silently lost. */
@media (max-width: 34rem) {
  .vcard {
    aspect-ratio: auto;
    max-width: none;
    padding: 1.5rem 1.5rem 1.5rem 1.75rem;
  }

  .vcard-foot {
    display: block;
  }

  .vcard-list {
    text-align: left;
    margin-top: 1.25rem;
  }
}

/* ---- footer ---- */
.footer {
  border-top: 1px solid var(--border);
  padding: 2rem 0;
  color: var(--text-muted);
  font-size: 0.82rem;
}

.footer p {
  margin: 0;
}

/* ---- project groups ----
   A level below .section-title: lowercase and unruled, so it reads as a quiet
   annotation on the cards rather than competing with the section heading. */
.group + .group { margin-top: 2.75rem; }

.group-label {
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0.12em;
  color: var(--text-muted);
  margin-bottom: 0.9rem;
}

/* Pinned to two columns rather than auto-fit: a group holding one card would
   otherwise stretch it to full width and make it look featured, wider than
   the pair above it. Below the breakpoint the base .cards rule stacks them. */
@media (min-width: 820px) {
  .group .cards {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ---- about facts ---- */
.facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.5rem 1.5rem;
  margin: 2rem 0 0;
  max-width: 44rem;
}

.facts dt {
  color: var(--accent-text);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding-top: 0.15rem;
}

.facts dd {
  margin: 0;
  color: var(--text);
  font-size: 0.95rem;
}

@media (max-width: 520px) {
  .facts {
    grid-template-columns: 1fr;
    gap: 0.15rem;
  }

  .facts dd {
    margin-bottom: 0.75rem;
  }
}

/* ---- page header (sub-pages) ---- */
/* padding-top only: a shorthand here would wipe .wrap's horizontal gutter
   and run the heading flush to the viewport edge on narrow screens. */
.page-head {
  padding-top: 3.5rem;
}

.page-head h1 {
  font-size: clamp(1.8rem, 5vw, 2.6rem);
  letter-spacing: -0.02em;
}

.lede {
  max-width: 44rem;
  margin: 1rem 0 0;
  color: var(--text-muted);
  font-size: 1rem;
}

/* A lede introducing one section rather than the whole page. Must come after
   .lede: both are single-class specificity, and placed above it this lost the
   cascade, so .lede's `margin: 1rem 0 0` zeroed the bottom margin and the
   cards sat flush against the paragraph.
   Top margin stays 0 so the 2rem from .section-title still sets the gap above;
   the bottom is wider than that, since a paragraph running into a card grid
   needs more air than a short label does. */
.section-lede {
  margin: 0 0 2.75rem;
}
