/*
 * Section-level layout. Mobile-first: base styles target 320px and every media query is
 * min-width (contract S-2, research R7). Breakpoints: base → 48rem → 64rem.
 *
 * Nothing here hides content at a smaller width. Where a layout needs to change, it changes
 * shape rather than disappearing (constitution VI, FR-034).
 *
 * PROPORTION — the rules this file now follows, and why.
 *
 * 1. Every collection grid is `auto-fit` with a `min()` floor rather than a fixed column
 *    count at a breakpoint. A fixed count is what orphans items: three columns strands the
 *    fifth of five, and two columns leaves a single project sitting in half a page. auto-fit
 *    fits as many equal columns as the *available* width allows, so the row always fills and
 *    the tracks stay identical to each other. It also removes several media queries outright.
 *
 * 2. Vertical rhythm comes from the fluid --space-* steps, so it grows with the viewport
 *    instead of stepping once at 48rem.
 *
 * 3. At the desktop breakpoint a section becomes two columns: a sticky heading and the
 *    content. Prose is capped at --measure for readability, and in a single 1152px column
 *    that cap left most of the page empty to the right of it. Moving the heading beside the
 *    content spends that width on the heading instead of leaving it blank — the content
 *    column then lands close to --measure on its own.
 */

/* Shared shell ---------------------------------------------------------- */

.site-header,
.hero,
.section,
.site-footer {
  padding-inline: var(--gutter);
}

.hero,
.section {
  max-width: var(--content-max);
  margin-inline: auto;
  padding-block: var(--space-2xl);
}

/*
 * A hairline between sections. With the generous padding above and below, adjacent sections
 * would otherwise read as one continuous column of text; the rule gives the eye the section
 * boundary without a heavy divider or a background change.
 *
 * `+` still matches when the previous section is `hidden` — that section renders nothing, so
 * the visible one that follows it is genuinely the next one, and gets its rule.
 */
.hero + .section,
.section + .section {
  border-block-start: var(--border-width) solid var(--color-border);
}

.section__heading {
  margin-block-end: var(--space-lg);
}

/* Grid and flex items default to `min-width: auto`, which refuses to shrink below the longest
   unbreakable word. Paired with the body's overflow-wrap, this is what keeps every section
   inside the viewport at 320px (FR-065). */
.section__body {
  min-width: 0;
}

/* Header and navigation -------------------------------------------------- */

/*
 * The nav is a wrapping flex row at every width. The pre-migration page hid the links
 * below the tablet breakpoint, which made every destination unreachable on a phone —
 * the single most common way this site is opened.
 */
.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background-color: var(--color-surface);
  border-block-end: var(--border-width) solid var(--color-border);
}

.site-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2xs) var(--space-md);
  min-height: var(--header-height);
  max-width: var(--content-max);
  margin-inline: auto;
  padding-block: var(--space-2xs);
}

.site-nav__brand {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  color: var(--color-text);
  font-size: var(--text-lg);
  font-weight: var(--font-weight-semibold);
  letter-spacing: -0.01em;
  text-decoration: none;
}

.site-nav__brand:hover {
  color: var(--color-accent);
}

/*
 * The disclosure toggle.
 *
 * `[data-collapsible]` is set by js/components/navigation.js, so every rule that hides the list
 * is scoped to it. Without the script the attribute never appears, the list stays visible, and
 * a no-script visitor gets every destination — taller, but complete (research R2).
 */
.site-nav__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-target);
  min-width: var(--touch-target);
  padding-inline: var(--space-sm);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius-sm);
  background-color: transparent;
  color: var(--color-text);
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}

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

.site-nav[data-collapsible="true"] .site-nav__list {
  display: none;
  /* Full-width row below the brand and toggle, so the links are not squeezed beside them. */
  flex-basis: 100%;
}

/*
 * Expanded, the mobile list is a COLUMN of full-width rows, not a wrapping row of short ones.
 * A wrapping row put ten destinations into three cramped lines whose targets sat a few pixels
 * apart — the arrangement `touch-spacing` and `no-precision-required` exist to prevent. A
 * column gives each destination the full width of the header and a 44px row.
 */
.site-nav[data-collapsible="true"] .site-nav__toggle[aria-expanded="true"] + .site-nav__list {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0;
  margin-block-start: var(--space-2xs);
  padding-block: var(--space-2xs);
  border-block-start: var(--border-width) solid var(--color-border);
}

.site-nav__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3xs) var(--space-2xs);
}

/*
 * 44px minimum on every destination (touch-target-size). These were inline links with 4px of
 * block padding — roughly a 26px target, and ten of them side by side.
 *
 * Muted rather than accent-coloured: ten accent links in a row is a band of colour competing
 * with the content beneath it. --color-text-muted on --color-surface is 7.4:1 (variables.css),
 * and the current destination is not signalled by colour alone — hover and focus both change
 * the background as well.
 */
.site-nav__list a {
  display: flex;
  align-items: center;
  min-height: var(--touch-target);
  padding-inline: var(--space-sm);
  border-radius: var(--radius-sm);
  /* Fixed 14px, not the fluid body step: ten destinations have to share one row with the
     brand, and every point of growth in the body scale pushes that row into wrapping
     earlier. The 44px height, not the type size, is what makes the target. */
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  text-decoration: none;
  transition: color var(--transition-fast), background-color var(--transition-fast);
}

.site-nav__list a:hover {
  background-color: var(--color-surface-accent);
  color: var(--color-text);
}

@media (min-width: 48rem) {
  /* Wide enough for the row: the disclosure is unnecessary, so the control goes away and the
     list is unconditionally visible again. */
  .site-nav__toggle {
    display: none;
  }

  .site-nav[data-collapsible="true"] .site-nav__list {
    display: flex;
    flex-basis: auto;
  }

  /* Tighter horizontal padding once the links share a row with the brand: the 44px height
     is what carries the target, and the row has to fit ten destinations. */
  .site-nav__list a {
    padding-inline: var(--space-2xs);
  }
}

/* Hero ------------------------------------------------------------------- */

.hero {
  display: grid;
  gap: var(--space-xl);
  /* More air above than below: the hero sits under a sticky header and needs to clear it, and
     the section that follows brings its own generous padding. */
  padding-block: var(--space-3xl) var(--space-2xl);
}

/*
 * `align-items: stretch`, NOT `flex-start`.
 *
 * flex-start sizes every child to fit-content, and a fit-content box has no width to wrap
 * against: an unbreakable string in the <h1> grew the box past the viewport and took the whole
 * document into horizontal scroll with it (FR-065, SC-011). `overflow-wrap: break-word` on the
 * body cannot save it, because there is no constraint for it to break at.
 *
 * Stretched, each child is as wide as the column and wraps inside it. The child that genuinely
 * needs to be content-sized — the actions row — says so itself with `flex-wrap`.
 */
.hero__intro {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  align-items: stretch;
  min-width: 0;
}

.hero__lede {
  font-size: var(--text-lg);
  color: var(--color-text-muted);
}

/*
 * The positioning statement is the largest thing on the page after the name, because it is the
 * claim the rest of the page argues for (FR-011, FR-012). Typography and hierarchy carry the
 * emphasis — no gradient, no decorative device (FR-070).
 */
.hero__headline {
  max-width: var(--measure);
  font-size: var(--text-xl);
  font-weight: var(--font-weight-semibold);
  line-height: var(--leading-tight);
  letter-spacing: -0.01em;
  color: var(--color-heading);
  margin-block-start: var(--space-2xs);
}

.hero__summary {
  max-width: var(--measure);
  line-height: var(--leading-relaxed);
  color: var(--color-text-muted);
}

/*
 * Wraps rather than scrolls: at 320px the two calls to action stack, which is why the container
 * is a wrapping flex row and not a grid with fixed tracks (FR-064, FR-065).
 */
.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-block-start: var(--space-2xs);
}

.social-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-lg);
  margin-block-start: var(--space-2xs);
}

.social-links__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  min-height: var(--touch-target);
  text-decoration: none;
}

.social-links__link:hover {
  text-decoration: underline;
}

/* Reserves the portrait's space before any image loads, so nothing shifts (FR-037). */
.hero__portrait-placeholder {
  aspect-ratio: 4 / 3;
  width: 100%;
  background-color: var(--color-surface);
  border-radius: var(--radius-lg);
}

.hero__portrait-image {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

/* Talks ------------------------------------------------------------------ */

/*
 * Same grid as .project-list, and for the reason stated at the top of this file: auto-fit with a
 * min() floor rather than a fixed column count, so the row always fills and a single talk stretches
 * to the column instead of sitting in half a page (FR-021).
 *
 * At the desktop breakpoint .section becomes two columns, so the body column this grid lives in is
 * around 50rem rather than the full 72rem — which is what keeps a lone card's photograph a
 * reasonable size rather than a full-bleed banner.
 */
.talk-list {
  display: grid;
  gap: var(--grid-gap);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}

/*
 * `min-width: 0` is load-bearing, not tidiness. As a grid item this defaults to `min-width: auto`,
 * which refuses to shrink below the longest unbreakable word — and the photograph's 1280px
 * intrinsic width would otherwise become the card's minimum and push the page into horizontal
 * scroll at 320px (FR-022). The overflow-wrap is the second half of the same fix, for a talk title
 * longer than the whole card.
 */
.talk {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  min-width: 0;
  overflow-wrap: anywhere;
}

/*
 * A fixed frame, not the file's own proportions. `aspect-ratio` plus the width/height attributes
 * the component sets reserves the box before the bytes land, so nothing below the card moves as
 * photographs arrive (FR-033). `object-fit: cover` is what lets a portrait, square or 4:3 source
 * fill that frame without being stretched (FR-019) — the owner drops in a photograph and never
 * measures anything.
 *
 * `height: auto` overrides the intrinsic height attribute; the attribute pair still carries the
 * ratio while the file loads.
 */
.talk__photo {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--radius-md);
  margin-block-end: var(--space-2xs);
}

/* Date and event on one line: the two facts scanned for after the title, so splitting them would
   give the card a second thin row saying almost nothing. */
.talk__meta {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

.talk__description {
  color: var(--color-text-muted);
  line-height: var(--leading-relaxed);
}

/* Pinned to the bottom like .project-card__links, so cards of differing text length keep their
   links on one line. The min-height is the touch target, not decoration (FR-029). */
.talk__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  margin-block-start: auto;
  padding-block-start: var(--space-2xs);
}

/* About ------------------------------------------------------------------ */

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

/* Capped at the measure so the two paragraphs stay readable rather than spanning the grid. */
.about__paragraph {
  max-width: var(--measure);
  line-height: var(--leading-relaxed);
  color: var(--color-text-muted);
}

/* Skills ----------------------------------------------------------------- */

/*
 * Six groups. auto-fit lands on three columns at desktop and two at tablet, which divides six
 * exactly either way — but the rule does not depend on there being six: add a seventh group and
 * the tracks stay identical, which a hardcoded `repeat(3, 1fr)` cannot promise.
 */
.skill-groups {
  display: grid;
  gap: var(--grid-gap);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}

.skill-group__name {
  margin-block-end: var(--space-md);
  font-size: var(--text-lg);
}

/* Selected Work ----------------------------------------------------------- */

.project-list {
  display: grid;
  gap: var(--grid-gap);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}

/*
 * `min-width: 0` is load-bearing, not tidiness: a grid item's default `min-width: auto` refuses
 * to shrink below its longest unbreakable word, which is how a long case-study title pushed the
 * whole document into horizontal scroll at 320px (FR-065, SC-011). The overflow-wrap below is
 * the second half of the same fix, for a single word longer than the whole card.
 */
.project-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  min-width: 0;
  overflow-wrap: anywhere;
}

.project-card__tagline {
  color: var(--color-accent);
  font-size: var(--text-sm);
}

.project-card__image {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--radius-md);
  margin-block-end: var(--space-2xs);
}

.project-card__description {
  line-height: var(--leading-relaxed);
  color: var(--color-text-muted);
}

.project-card__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin-block-start: auto;
  padding-block-start: var(--space-sm);
}

/* Engineering Philosophy --------------------------------------------------- */

/*
 * One column at every width, deliberately.
 *
 * This was two columns at the desktop breakpoint, and there are five principles — so the fifth
 * sat alone in a half-width cell, which is exactly the asymmetry the layout was meant to avoid.
 * A stated position also needs its full measure to be read as an argument rather than a slogan,
 * so the single column is the better shape for the content as well as the better shape on screen.
 */
.principle-list {
  display: grid;
  gap: var(--space-lg);
}

.principle {
  min-width: 0;
  overflow-wrap: anywhere;
  padding-inline-start: var(--space-md);
  border-inline-start: calc(var(--border-width) * 2) solid var(--color-border);
}

.principle__title {
  font-size: var(--text-lg);
  line-height: var(--leading-tight);
  margin-block-end: var(--space-2xs);
}

.principle__detail {
  max-width: var(--measure);
  line-height: var(--leading-relaxed);
  color: var(--color-text-muted);
}

/* Community ---------------------------------------------------------------- */

.community-list {
  display: grid;
  gap: var(--grid-gap);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
}

.community {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  min-width: 0;
  overflow-wrap: anywhere;
}

.community__organisation {
  font-size: var(--text-lg);
}

.community__contribution {
  color: var(--color-accent);
  font-size: var(--text-sm);
}

.community__period {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

.community__description {
  max-width: var(--measure);
  line-height: var(--leading-relaxed);
  color: var(--color-text-muted);
}

.community__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  padding-block-start: var(--space-sm);
}

/* Certifications --------------------------------------------------------- */

/*
 * One entry per row, matching `.timeline` in Experience and Education rather than the auto-fit
 * columns this list used to take. Side-by-side columns halved the width available to a card,
 * which was harmless while every entry was two lines of text and is not now that one carries a
 * photograph: at half width the evidence is too small to read anything in.
 *
 * The `max-width: var(--measure)` this list carried before the columns is deliberately still
 * absent — 68ch is a measure for prose, and it made a two-entry list sit as a narrow strip in
 * an otherwise empty section.
 */
.certification-list {
  display: grid;
  gap: var(--grid-gap);
}

.certification {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
}

/*
 * `min-width: 0` is load-bearing, not tidying: as a flex item this defaults to `min-width: auto`,
 * so the evidence photo's 1000px intrinsic width would become the card's minimum and push the
 * page into horizontal scroll (FR-065, SC-010).
 */
.certification__body {
  min-width: 0;
}

.certification__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--radius-md);
  background-color: var(--color-surface-accent);
  color: var(--color-text);
}

.certification__issuer,
.certification__detail {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

.certification__title {
  margin-block-end: var(--space-3xs);
}

.certification__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  margin-block-start: var(--space-2xs);
}

/*
 * The evidence photo sits below the claim rather than beside it: the badge already anchors the
 * left of the card, and a second column there would leave the photograph too narrow to read.
 *
 * `height: auto` overrides the intrinsic `height` attribute; the attribute pair still reserves
 * the aspect ratio while the file loads, so nothing below it jumps (C-8). The cap keeps the
 * photo from spanning the full row on a wide screen, where it would outweigh the claim it is
 * only meant to support — and stops it being scaled past its own 1000px, which buys no detail
 * and only softens the image.
 */
.certification__evidence {
  display: block;
  width: 100%;
  max-width: 32rem;
  height: auto;
  margin-block-start: var(--space-xs);
  border-radius: var(--radius-md);
}

/* Experience and education ----------------------------------------------- */

.timeline {
  display: grid;
  gap: var(--grid-gap);
}

.timeline__dates,
.timeline__location,
.timeline__institution {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

.timeline__title {
  margin-block-end: var(--space-2xs);
}

.timeline__summary {
  max-width: var(--measure);
  margin-block-start: var(--space-sm);
  line-height: var(--leading-relaxed);
}

.timeline__achievements {
  display: grid;
  gap: var(--space-2xs);
  max-width: var(--measure);
  margin-block-start: var(--space-sm);
  padding-inline-start: var(--space-md);
  line-height: var(--leading-relaxed);
  list-style: disc;
}

/* Contact ---------------------------------------------------------------- */

/*
 * The closing call to action (FR-042): prominent through hierarchy and spacing, not through
 * sales devices. No gradient, no oversized panel, no urgency.
 */
/*
 * Four distinct routes rather than one button and some prose (FR-040). Wraps at every width, so
 * a route is never pushed off-screen or truncated at 320px.
 */
/* Last block in the section now that the form is gone, so no trailing margin: the section's own
   padding supplies the space below it. */
.contact-routes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.contact-details {
  display: grid;
  max-width: var(--measure);
  margin-block-end: var(--space-xl);
}

/* A labelled row rather than two inline spans: the label column lines up between rows, which
   is what makes a two-row list read as a table of facts instead of two stray sentences. */
.contact-details__item {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-md);
  padding-block: var(--space-xs);
  border-block-end: var(--border-width) solid var(--color-border);
}

.contact-details__label {
  min-width: 6rem;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* Footer ----------------------------------------------------------------- */

.site-footer {
  background-color: var(--color-surface);
  border-block-start: var(--border-width) solid var(--color-border);
  padding-block: var(--space-xl);
}

.site-footer__copyright {
  max-width: var(--content-max);
  margin-inline: auto;
  color: var(--color-text-muted);
  text-align: center;
  font-size: var(--text-sm);
}

/* Tablet ----------------------------------------------------------------- */

@media (min-width: 48rem) {
  /*
   * Close to 1:1 rather than the 3:2 it was. The portrait is a 4/3 illustration; starving it to
   * two fifths of the row while the text column ran well past its measure was the widest of the
   * proportion problems on this page.
   */
  .hero {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
    align-items: center;
    gap: var(--space-2xl);
    padding-block: var(--space-3xl) var(--space-2xl);
  }
}

/* Desktop ---------------------------------------------------------------- */

@media (min-width: 64rem) {
  /*
   * Two columns: the heading beside its content, not above it.
   *
   * The heading sticks while its section scrolls, so the reader always knows which section they
   * are in — the sticky position works because the heading's grid area spans the full row height
   * even though `align-items: start` keeps the box itself at the top of it.
   *
   * `column-gap` only, never `row-gap`: the content column's children space themselves with
   * their own margins, and a row-gap here would add to those and double the spacing.
   */
  .section {
    display: grid;
    grid-template-columns: minmax(0, var(--section-label)) minmax(0, 1fr);
    column-gap: var(--space-xl);
    align-items: start;
  }

  .section__heading {
    position: sticky;
    top: var(--sticky-offset);
    margin-block-end: 0;
  }

  /* Everything that is not the heading belongs to the content column. Written as a negation so
     a section that later authors a second block beside .section__body still lands correctly. */
  .section > :not(.section__heading) {
    grid-column: 2;
  }
}
