:root {
  --ink: #0B0B0C;
  --ink-deep: #060607;
  --ink-panel: #0F0F10;
  --gold: #D4A94F;
  --gold-bright: #E9C87A;
  /* Text gold for light backgrounds — darkened from #B08A34, which only reached
     3.2:1 on white and failed WCAG 1.4.3. --gold-line keeps the original tone for
     decorative rules, which carry no contrast requirement. */
  --gold-dark: #896B28;
  --gold-line: #B08A34;
  --cream: #F5F3EC;
  --body: #E9E9E6;
  --muted: #C9C9C4;
  --muted-2: #A5A5A0;
  --muted-3: #8A8A85;
  /* Was #6B6B66 — 3.8:1 on the destinations background, under the 4.5:1 minimum. */
  --muted-4: #797974;
  --serif: 'Cormorant Garamond', serif;
  --sans: 'Jost', sans-serif;
  --mono: 'IBM Plex Mono', monospace;
  --shell: 1180px;
  --gutter: clamp(20px, 4.5vw, 32px);
  --section-y: clamp(64px, 10vw, 110px);
  /* 60% of --section-y: the sections' own vertical padding after the 40% trim.
     Derived with calc rather than restated as its own clamp so the two can never
     drift apart. The full-size token still governs the experience/aircraft join,
     which was deliberately left at the original spacing. */
  --section-y-tight: calc(var(--section-y) * 0.6);
  /* Parallax travel in each direction. The image is grown by twice this and pulled
     up by one, so it still covers its frame at rest and at both extremes. */
  --parallax: clamp(36px, 5vw, 72px);
}

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;

  /* Suppresses the rubber-band overscroll at both ends. At the foot of the page
     main's bottom edge rests exactly on the fixed contact panel's top edge; the
     bounce translates the document up while the panel stays pinned to the
     viewport, opening a gap between them. No bounce, no gap.
     (Also disables Android pull-to-refresh, which this static page has no use for.) */
  overscroll-behavior-y: none;

  /* Belt and braces for anything that ignores the above: the canvas is what shows
     through such a gap, so it matches the destinations section rather than the
     slightly lighter body colour, making the band invisible if it ever appears. */
  background: var(--ink-deep);
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--body);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--gold); text-decoration: none; }
a:hover { color: var(--gold-bright); }

img { display: block; max-width: 100%; }

::selection { background: var(--gold); color: var(--ink); }

h1, h2 { font-family: var(--serif); font-weight: 600; line-height: 1.1; margin: 0; }

/* Parallax frames. The frame owns the geometry — including the edges the washes are
   aligned to, like the hero's 80% split — and never moves. The image inside is taller
   than the frame and slides within it, so the composition's boundaries stay put while
   the picture drifts. Both frames clip, so the overhang is never visible. */
.hero__media,
.exp__media {
  position: absolute;
  overflow: hidden;
}

.hero__photo,
.exp__photo {
  width: 100%;
  height: calc(100% + var(--parallax) * 2);
  margin-top: calc(var(--parallax) * -1);
  object-fit: cover;
  will-change: transform;
}

.shell {
  max-width: var(--shell);
  margin: 0 auto;
  padding-inline: var(--gutter);
}

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

/* WCAG 2.4.7 — one visible indicator for every interactive element. Two-tone so it
   holds up on both the dark sections and the white/ivory ones. */
:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 3px;
  border-radius: 2px;
}

.about :focus-visible,
.cred :focus-visible {
  outline-color: #121212;
}

/* WCAG 2.4.1 — a way past the fixed masthead to the content. */
.skip-link {
  position: absolute;
  left: 50%;
  top: 0;
  z-index: 200;
  transform: translate(-50%, -110%);
  padding: 12px 22px;
  background: var(--gold);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.2em;
  transition: transform 0.18s ease;
}

.skip-link:focus {
  transform: translate(-50%, 0);
  color: var(--ink);
}

main:focus { outline: none; }

@keyframes fadeUp { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } }
@keyframes fadeSelA { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes fadeSelB { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* ---------- shared section furniture ---------- */

.eyebrow {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 22px;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.3em;
  color: var(--gold);
}

.eyebrow::before,
.eyebrow--centered::after {
  content: "";
  display: inline-block;
  width: 44px;
  height: 1px;
  background: currentColor;
  flex: none;
}

.eyebrow--centered { justify-content: center; }
.eyebrow--dark { color: var(--gold-dark); }

.section-title { font-size: clamp(36px, 3.6vw, 52px); color: var(--cream); }
.section-title--dark { color: #121212; }
.section-title--centered { text-align: center; margin-inline: auto; }

/* ---------- nav ---------- */

.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 18px clamp(20px, 4vw, 40px);
  background: rgba(8, 8, 9, 0.82);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid rgba(212, 169, 79, 0.16);
}

.nav__brand {
  font-family: var(--serif);
  font-size: 21px;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--cream);
}

.nav__links {
  display: flex;
  gap: 30px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.2em;
}

/* Vertical padding lifts these from an 11px text box to a 24px target (WCAG 2.5.8). */
.nav__links a { color: var(--muted); padding-block: 6px; }
.nav__links a:hover { color: var(--gold); }

.nav__burger {
  display: none;
  flex-direction: column;
  gap: 5px;
  padding: 8px 2px;
  background: none;
  border: 0;
  cursor: pointer;
}

.nav__burger span {
  display: block;
  width: 22px;
  height: 2px;
  background: var(--body);
  transition: transform 0.28s ease, opacity 0.18s ease;
}

/* Burger → X, keyed off the aria state the JS already maintains, so no extra
   class or script is needed. The spans sit on a 7px pitch (2px bar + 5px gap);
   the outer pair translate onto the middle line and rotate, the middle bar fades. */
.nav__burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav__burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav__burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* The wrapper owns the old absolute positioning and clips the slide, so the menu
   can hide at translateY(-100%) — tucked up behind the bar — without painting over
   it. pointer-events:none keeps the (invisible but full-height) box from eating
   taps on the hero; the menu itself restores hit-testing when visible. */
.nav__menuwrap {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  overflow: hidden;
  pointer-events: none;
}

.nav__menu {
  display: flex;
  flex-direction: column;
  padding: 8px 24px 16px;
  background: rgba(8, 8, 9, 0.96);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid rgba(212, 169, 79, 0.2);
  pointer-events: auto;
  transform: translateY(-100%);
  visibility: hidden;
  /* visibility flips only after the slide finishes; on open the delay is zeroed
     so links are hittable the moment the slide starts. */
  transition: transform 0.32s ease, visibility 0s linear 0.32s;
}

.nav__menu.is-open {
  transform: none;
  visibility: visible;
  transition: transform 0.32s ease, visibility 0s;
}

@media (prefers-reduced-motion: reduce) {
  .nav__burger span,
  .nav__menu { transition: none; }
}

.nav__menu a {
  padding: 13px 0;
  border-bottom: 1px solid rgba(245, 243, 236, 0.08);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.22em;
  color: var(--body);
}

.nav__menu a:last-child { border-bottom: 0; }

@media (max-width: 879px) {
  .nav__links { display: none; }
  .nav__burger { display: flex; }
}

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

.hero {
  position: relative;
  min-height: 100vh;
  display: flex;
  align-items: center;
  overflow: hidden;
  background: var(--ink);
}

/* The photo occupies the right 80% only; the left 20% stays the section's black.
   object-position centres the displayed crop on the 62%/35% point of the source. This
   is the landscape framing; portrait overrides the horizontal half below, because the
   narrower frame crops away far more and needs a different value to sit the same way.
   Full opacity; the wash below controls how much of the photo reads. */
.hero__media {
  /* Deeper travel than the root default, hero only: custom properties inherit, so
     redeclaring the var on this frame reaches the .hero__photo sizing rule without
     touching the experience photo. The JS reads travel back from layout, so it
     picks up the larger overhang on its own. */
  --parallax: clamp(48px, 6.5vw, 96px);
  top: 0;
  bottom: 0;
  left: 20%;
  width: 80%;
}

.hero__photo {
  object-position: 62% 35%;
  opacity: 1;
}

/* A long, gradual wash: solid black across the photo's left edge, reaching fully
   transparent only at the far right so the fade never reads as a hard seam.
   90deg runs the gradient straight left-to-right, so its bands are exactly
   vertical — at the previous 100deg they leaned 10 degrees off true. */
.hero__scrim {
  position: absolute;
  inset: 0;
  /* Fully opaque through 24%, which is past the photo's left edge at 20%. At 0.97
     the photo still contributed ~3% right at that boundary while the strip beside it
     was pure background — a straight vertical line the eye picks out even though the
     luminance step is tiny. Carrying full opacity across the seam removes it. */
  /* Mid stops +0.02 were tuned for the previous 4:3 arrival photo, whose bright
     fuselage ran behind the headline's right end — 0.88/0.68 left the gold at 4.38:1
     there; the bump brought the measured floor back to 4.8:1. Not yet re-measured
     against the current 2048×2568 sky-extended photo. */
  background: linear-gradient(90deg,
    rgba(6, 6, 7, 1) 0%,
    rgba(6, 6, 7, 1) 24%,
    rgba(6, 6, 7, 0.9) 36%,
    rgba(6, 6, 7, 0.7) 55%,
    rgba(6, 6, 7, 0.26) 78%,
    rgba(6, 6, 7, 0) 100%);
}

.hero__fade {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 180px;
  background: linear-gradient(to bottom, rgba(11, 11, 12, 0) 0%, var(--ink) 100%);
}

.hero__inner {
  position: relative;
  z-index: 2;
  width: 100%;
  padding-block: 140px 100px;

  /* Past the shell's 1180px ceiling the copy would centre along with every other
     section. Here it sits half that distance from the left instead, so it stays
     weighted to the edge on wide screens. max() makes the rule inert below 1180px,
     where there is no spare width to redistribute. Percentage rather than vw: it
     resolves against the hero, so a desktop scrollbar cannot skew it. */
  margin-left: max(0px, calc((100% - var(--shell)) / 4));
  margin-right: auto;
}

.hero__content { animation: fadeUp 0.9s 0.15s both; }

.hero .eyebrow::before { width: 32px; }

.hero h1 {
  margin-bottom: 30px;
  font-size: clamp(52px, 7vw, 92px);
  line-height: 1.04;
  color: var(--cream);
}

.hero h1 em { color: var(--gold); font-weight: 500; }

.hero__lede {
  margin: 0 0 42px;
  max-width: 520px;
  font-size: 18px;
  font-weight: 300;
  line-height: 1.65;
  color: var(--muted);
  text-wrap: pretty;
}

.hero__actions { display: flex; flex-wrap: wrap; gap: 18px; }

.btn {
  display: inline-block;
  padding: 15px 30px;
  border: 1px solid;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.22em;
  transition: background 0.25s, color 0.25s, border-color 0.25s;
}

.btn--gold { border-color: rgba(212, 169, 79, 0.65); color: var(--gold); }
.btn--gold:hover { background: var(--gold); color: var(--ink); }

/* 0.45 rather than 0.30: the border is the button's only visible boundary, so it
   needs 3:1 against the backdrop under WCAG 1.4.11. */
.btn--ghost { border-color: rgba(233, 233, 230, 0.45); color: var(--body); }
.btn--ghost:hover { border-color: var(--cream); color: var(--cream); }

/* On an upright viewport a full 100vh hero pushes everything else below the fold,
   so cap it at 16 units tall per 9 wide. min() keeps 100vh as the ceiling — on a
   nearly-square viewport 16/9 of the width would otherwise be taller than the
   screen, which is the opposite of a cap. It stays min-height, so unusually large
   text grows the section rather than being clipped by the section's overflow. */
@media (orientation: portrait) {
  .hero {
    min-height: min(100vh, calc(100vw * 16 / 9));
    /* Copy sits on the floor of the frame, under the photo rather than beside it. */
    align-items: flex-end;
  }

  /* Bottom gap set from --gutter, the same token .shell uses for padding-inline, so
     the space under the button tracks the gutter — at twice its size, since the
     original one-gutter clearance read too tight under the button. The top padding
     no longer positions anything, but it is what guarantees nav clearance in the
     one case where the hero grows past the cap to fit its content on a very
     narrow screen. */
  .hero__inner { padding-block: 116px calc(var(--gutter) * 2); }

  /* Tightened from 22/30/42 — the stack reads as one block against the black band
     rather than four loosely spaced pieces. The title's 22px was solved from ink,
     not boxes: with the descenders (g/p droop) of "through experience." factored
     in, it puts the title-ink-to-lede-ink gap at ~24.9px — the same distance as
     lede-ink-to-button-border. The two gaps match visually even though the
     margins (22/20) don't quite. */
  .hero .eyebrow { margin-bottom: 8px; }
  .hero h1 { margin-bottom: 22px; }
  .hero__lede { margin-bottom: 20px; }

  /* The mirror of landscape: there the photo takes the right 80% and the left 20%
     stays black; here it takes the top 80% and the bottom 20% is the black the copy
     sits on. bottom:auto releases the base rule's bottom:0 so height can govern.
     66% rather than the base 62%: this frame is much narrower relative to the source,
     so it discards roughly 600px of width against landscape's 400, and the same
     percentage would not land the subject in the same place. Which half is live
     depends on the frame's aspect against the source's (2048×2568 ≈ 0.80): on phone
     portrait (frame ≈ 0.52) the full height fits and only width crops, so the
     vertical value is inert; on wider portrait frames like tablets (≈ 0.86 > 0.80)
     the axis flips — width fits, height crops, and the vertical 40% governs. */
  .hero__media {
    /* Double the landscape hero travel — same scoping trick as the base rule; the
       experience photo still keeps the root value. */
    --parallax: clamp(96px, 13vw, 192px);
    top: 0;
    bottom: auto;
    left: 0;
    width: 100%;
    height: 80%;
  }

  .hero__photo { object-position: 66% 40%; }

  /* 0deg runs bottom-to-top: solid at the floor, clear at the top.
     These are the landscape stops re-anchored, not new ones. Horizontally the copy
     reaches ~56% and the wash is still 0.68 there before tailing off across the
     remainder; the copy here reaches ~53%, so each stop is rescaled to that — the
     same falloff, just measured against a shorter run. The result keeps the dark
     essentially behind the text and hands the rest of the frame back to the photo. */
  .hero__scrim {
    background: linear-gradient(0deg,
      rgba(6, 6, 7, 1) 0%,
      rgba(6, 6, 7, 1) 24%,
      rgba(6, 6, 7, 0.88) 37%,
      rgba(6, 6, 7, 0.68) 56%,
      rgba(6, 6, 7, 0.26) 78%,
      rgba(6, 6, 7, 0) 100%);
  }
}

/* ---------- stats ---------- */

.stats {
  background: var(--ink-panel);
  border-block: 1px solid rgba(212, 169, 79, 0.16);
}

.stats__grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 24px;
  padding-block: 26px;
}

.stat { text-align: center; border-right: 1px solid rgba(212, 169, 79, 0.14); }
.stat:last-child { border-right: 0; }

.stat__value {
  font-family: var(--serif);
  font-size: 38px;
  font-weight: 600;
  line-height: 1;
  color: var(--gold);
}

.stat__label {
  margin-top: 7px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.26em;
  color: var(--muted-3);
}

/* Five stats wrap 3+2, then 2+2+1. Each row is laid out on a double-width track
   so the short trailing row can be offset half a column and sit centred. */
@media (max-width: 1000px) {
  .stats__grid { grid-template-columns: repeat(6, 1fr); gap: 22px; }
  .stat { grid-column: span 2; border-right: 0; }
  .stat:nth-child(4) { grid-column: 2 / span 2; }
  .stat:nth-child(5) { grid-column: 4 / span 2; }
}

@media (max-width: 620px) {
  .stats__grid { grid-template-columns: repeat(4, 1fr); gap: 20px; }
  .stat:nth-child(4) { grid-column: span 2; }
  .stat:nth-child(5) { grid-column: 2 / span 2; }
}

/* ---------- about ---------- */

.about {
  background: #FFFFFF;
  color: #1A1A18;
  scroll-margin-top: 70px;
}

/* Single flow rather than two columns: the portrait floats out of the copy, so
   the eyebrow and heading run the full measure above it, and the paragraphs wrap
   down the side and then continue underneath. */
.about__body {
  padding-block: var(--section-y-tight);
}

/* Two different orders either side of the breakpoint.
   Floated (wide): label, then heading, both full measure — then the photo floats
   level with the first paragraph, so source order alone gives the right result.
   Stacked (phones): label, photo, heading, copy — the photo moves up between the
   label and the heading, which source order can't express without breaking the
   float. Flex ordering handles it, and is confined here because a flex container
   cannot host a float. */
@media (max-width: 719px) {
  .about__body { display: flex; flex-direction: column; }
  .about__body > .eyebrow       { order: 1; }
  .about__body > .about__frame  { order: 2; }
  .about__body > .section-title { order: 3; }
  .about__body > p:not(.eyebrow){ order: 4; }
  .about__body > .signature     { order: 5; }
}

/* Contains the float — without it the section's bottom padding collapses against
   a portrait taller than the text beside it. Only needed where a float exists. */
@media (min-width: 720px) {
  .about__body::after {
    content: "";
    display: block;
    clear: both;
  }
}

/* :not(.eyebrow) is load-bearing — the section label is also a <p>, and ".about p"
   outranks ".eyebrow" on specificity, so without it the label renders as body copy
   instead of the 12px gold rule-and-caption every other section uses. */
.about p:not(.eyebrow) {
  margin: 0 0 20px;
  font-size: 16.5px;
  font-weight: 300;
  line-height: 1.75;
  color: #3E3E3C;
  text-wrap: pretty;
}

.about p:last-of-type { margin-bottom: 34px; }

.about .section-title { margin-bottom: 26px; }

.signature {
  font-family: 'Beau Rivage', cursive;
  font-size: 54px;
  font-style: italic;
  font-weight: 500;
  line-height: 1;
  color: #121212;
}

/* Narrow viewports: the portrait sits in the flow between heading and copy, centred.
   85vw keeps it inside the gutters on phones; the 380px ceiling stops it ballooning
   to near the full page width in the last stretch before the float kicks in, where
   85vw alone would have made it 611px. Capping the frame rather than the img keeps
   the offset rule aligned to the photo. */
.about__frame {
  position: relative;
  padding: 0 18px 18px 0;
  max-width: min(85vw, 380px);
  margin: 0 auto 30px;
}

/* Wide enough for a usable column beside the photo — below this the text would be
   squeezed into a ragged strip, so the float is only introduced here. */
@media (min-width: 720px) {
  .about__frame {
    float: right;
    /* The 300px floor stops the photo collapsing to a thumbnail in the crowded
       stretch just above the breakpoint, and keeps the step down from the 380px
       stacked size small. The ceiling stays tight on purpose: the portrait is
       4:4.6, so every pixel of width costs 1.15 of height — widen it and it
       outgrows the copy, and the text stops clearing its bottom edge. */
    width: clamp(300px, 32%, 350px);
    max-width: none;
    /* The heading's descenders sit just above; a few px keeps the photo's top edge
       optically level with the first line of copy rather than the heading box. */
    margin: 6px 0 30px clamp(32px, 4vw, 56px);
  }
}

.about__frame::before {
  content: "";
  position: absolute;
  top: 18px; left: 18px; right: 0; bottom: 0;
  border: 1px solid var(--gold-line);
}

.about__frame img {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 4.6;
  object-fit: cover;
}

/* ---------- experience ---------- */

.exp {
  position: relative;
  background: var(--ink);
  overflow: hidden;
  scroll-margin-top: 70px;
}

.exp__media,
.exp__scrim,
.exp__fade { position: absolute; right: 0; width: 52%; }

.exp__media { top: 0; bottom: 0; }
.exp__photo { opacity: 0.9; }
.exp__scrim { top: 0; bottom: 0; background: linear-gradient(to right, var(--ink) 0%, rgba(11, 11, 12, 0.55) 45%, rgba(11, 11, 12, 0.12) 100%); }
.exp__fade { bottom: 0; height: 160px; background: linear-gradient(to bottom, rgba(11, 11, 12, 0) 0%, var(--ink) 100%); }

.exp__inner {
  position: relative;
  z-index: 2;
  /* Top trimmed with the rest; the bottom is half of the experience/aircraft join
     and deliberately keeps its original value. */
  padding: var(--section-y-tight) clamp(20px, 4.5vw, 40px) clamp(40px, 6vw, 64px);
}

.exp__text { width: 46%; min-width: 300px; }
.exp .section-title { margin-bottom: 44px; }

/* Scroll-linked fill: the rail sits white, and --fill (set in JS from scroll
   progress) draws the gold overlay down it. Dots light as the fill reaches them. */
.timeline {
  position: relative;
  border-left: 1px solid rgba(245, 243, 236, 0.3);
  display: flex;
  flex-direction: column;
  gap: 40px;
}

.timeline::after {
  content: "";
  position: absolute;
  left: -1px;
  top: 0;
  width: 1px;
  height: var(--fill, 0px);
  background: var(--gold);
}

.tl-item { position: relative; padding-left: 32px; }

.tl-item::before {
  content: "";
  position: absolute;
  left: -5px;
  top: 7px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--cream);
  box-shadow: 0 0 0 0 rgba(212, 169, 79, 0);
  transition: background 0.45s ease, box-shadow 0.45s ease;
}

.tl-item.is-lit::before {
  background: var(--gold);
  box-shadow: 0 0 0 4px rgba(212, 169, 79, 0.16);
}

/* The entry's copy arrives with its dot. Children keep their space while hidden,
   so lighting a dot never reflows the column.
   Gated on .is-animated, which the script adds: without JS nothing ever sets
   .is-lit, and hiding by default would leave the copy permanently unreadable. */
.timeline.is-animated .tl-item > * {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.55s ease, transform 0.55s ease;
}

.timeline.is-animated .tl-item.is-lit > * {
  opacity: 1;
  transform: none;
}

.tl-item__years {
  margin-bottom: 8px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.24em;
  color: var(--gold);
}

.tl-item__role {
  margin-bottom: 4px;
  font-family: var(--serif);
  font-size: 27px;
  font-weight: 600;
  color: var(--cream);
}

.tl-item__org {
  margin-bottom: 10px;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--muted-2);
}

.tl-item p {
  margin: 0;
  font-size: 15px;
  font-weight: 300;
  line-height: 1.7;
  color: var(--muted-3);
  text-wrap: pretty;
}

@media (max-width: 900px) {
  .exp__media { width: 100%; }
  .exp__photo { opacity: 0.22; }
  .exp__scrim { width: 100%; background: rgba(11, 11, 12, 0.6); }
  .exp__fade { width: 100%; }
  .exp__text { width: 100%; min-width: 0; }
}

/* ---------- aircraft ---------- */

.aircraft {
  background: linear-gradient(180deg, var(--ink) 0%, #101012 200px, #101012 100%);
  scroll-margin-top: 70px;
}

/* Top keeps its original value — the other half of the experience/aircraft join.
   Bottom carries +12px: the experience section's visual top gap is its padding plus
   the eyebrow's un-reset UA margin-top (1em at 12px), and this section's last row
   ends in a bare border with no such margin. Measured 53.5px vs 41.5px before the
   correction; the +12px squares them. */
.aircraft .shell { padding-block: clamp(40px, 6vw, 64px) calc(var(--section-y-tight) + 12px); }
.aircraft .section-title { margin-bottom: 54px; }

/* The two columns are sized to their content and the pair is centred as a unit,
   so the block sits on the page's centre line at every width. */
.air__grid {
  display: grid;
  grid-template-columns: minmax(190px, 240px) minmax(0, 560px);
  justify-content: center;
  gap: clamp(32px, 5vw, 56px);
  align-items: start;
}

.air__menu {
  display: flex;
  flex-direction: column;
  padding-right: 28px;
  border-right: 1px solid rgba(245, 243, 236, 0.15);
}

.air__item {
  padding: 11px 0 11px 14px;
  border: 0;
  border-left: 2px solid transparent;
  background: none;
  text-align: left;
  font-family: var(--serif);
  font-size: 22px;
  font-weight: 400;
  color: #B9B9B4;
  cursor: pointer;
  transition: color 0.2s, border-color 0.2s;
}

.air__item:hover { border-left-color: rgba(212, 169, 79, 0.4); color: var(--cream); }

.air__item[aria-selected="true"] {
  border-left-color: var(--gold);
  color: var(--gold);
  font-weight: 600;
}

.air__spot--a { animation: fadeSelA 0.5s ease; }
.air__spot--b { animation: fadeSelB 0.5s ease; }

.air__label {
  margin-bottom: 14px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.26em;
  color: var(--muted-3);
}

.air__name {
  margin-bottom: 28px;
  font-family: var(--serif);
  font-size: clamp(40px, 5vw, 64px);
  font-weight: 600;
  line-height: 1;
  color: var(--cream);
}

.air__specs {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: 560px;
  margin: 0;
}

.air__specs > div {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  padding-bottom: 12px;
  border-bottom: 1px solid rgba(245, 243, 236, 0.15);
}

.air__specs dt {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.24em;
  color: var(--gold);
}

.air__specs dd { margin: 0; font-size: 15px; color: var(--body); }

/* Below the two-column breakpoint the six aircraft become an even grid — 3x2, then
   2x3 — so the buttons stay symmetric instead of wrapping into a ragged last row. */
@media (max-width: 900px) {
  .air__grid { grid-template-columns: minmax(0, 560px); justify-content: center; gap: 36px; }

  .air__menu {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 4px 10px;
    padding: 0 0 18px;
    border-right: 0;
    border-bottom: 1px solid rgba(245, 243, 236, 0.15);
  }

  .air__item {
    padding: 11px 6px;
    text-align: center;
    border-left: 0;
    border-bottom: 2px solid transparent;
  }

  .air__item:hover { border-bottom-color: rgba(212, 169, 79, 0.4); }
  .air__item[aria-selected="true"] { border-bottom-color: var(--gold); }

  .air__label,
  .air__name { text-align: center; }

  .air__specs { margin-inline: auto; }
}

@media (max-width: 560px) {
  .air__menu { grid-template-columns: repeat(2, 1fr); }
}

/* ---------- credentials ---------- */

.cred {
  background: #F6F6F4;
  color: #1A1A18;
  scroll-margin-top: 70px;
}

.cred .shell { padding-block: var(--section-y-tight); }
.cred .section-title { margin-bottom: 48px; max-width: 640px; }

.cred__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  max-width: 900px;
  margin: 0 auto;
  background: #FFFFFF;
  border: 1px solid rgba(18, 18, 18, 0.16);
  box-shadow: 0 14px 36px rgba(18, 18, 18, 0.09);
}

.cred__cell {
  padding: 20px 24px;
  border-right: 1px dashed rgba(18, 18, 18, 0.28);
  border-bottom: 1px dashed rgba(18, 18, 18, 0.28);
  transition: background 0.25s;
}

.cred__cell:hover { background: #FAF8F1; }

/* Two-up: drop the dashes that would otherwise run along the outer frame. Six
   cells fill three rows exactly, so the last row is simply the final two — no
   cell needs to span to avoid leaving a hole. */
.cred__cell:nth-child(2n) { border-right: 0; }
.cred__cell:nth-last-child(-n + 2) { border-bottom: 0; }

.cred__kind {
  margin-bottom: 10px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.26em;
  color: var(--gold-dark);
}

/* Each cell carries a list of entries rather than a single title, so a category
   like Type Ratings can hold any number of them and grow to fit. */
.cred__entries {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.cred__entry {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
}

.cred__name {
  font-family: var(--serif);
  font-size: 24px;
  font-weight: 600;
  color: #121212;
}

/* Credentials typewriter (structure built by script.js; static markup untouched
   for no-JS and reduced-motion visitors). The ghost keeps each entry's exact box
   and wrap points while its visible twin types over it, so nothing reflows as
   characters appear; a visually-hidden copy carries the plain text for screen
   readers throughout. */
.typer { position: relative; }
.typer__ghost { visibility: hidden; }
.typer__live { position: absolute; inset: 0; }

/* Caret on the line currently being typed: steady while characters flow,
   blinking (--blink) during the 2s idle bookends before and after the run. */
.typer__live--on::after {
  content: "";
  /* A plain inline with a painted border, not an inline-block: atomic inlines
     admit a line break before them even without whitespace, and because each
     entry's box shrink-wraps its text there is no slack — a 4px atomic caret
     always broke onto its own line. A non-atomic empty inline with no space
     before it cannot break away from the word: it hugs the last glyph and
     overflows the box by its own width instead. */
  border-left: 2px solid currentColor;
  margin-left: 2px;
  /* Cancels the 4px the caret adds to the word's unbreakable width. Without
     this, a line the text fills exactly (mobile) re-wraps at the preceding
     space while the caret is attached — the word jumps lines until the flash
     ends. Net-zero advance means the line measures as if the caret weren't
     there; it just paints past the last glyph. */
  margin-right: -4px;
}

/* 0.8s cycle: two and a half flashes per 2s bookend. */
.typer__live--blink::after { animation: typerBlink 0.8s step-end infinite; }

@keyframes typerBlink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

/* Stacked tags for an entry qualified by more than one thing. Right-aligned so
   the boxes line up, and the entry's baseline alignment ties the first tag to the
   entry name. */
.cred__tags {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
}

/* Privilege tag. Outlined is the default; --priv fills it, so the level a rating
   is held at reads at a glance instead of hiding in a sub-line. */
.cred__tag {
  flex-shrink: 0;
  padding: 3px 8px;
  border: 1px solid rgba(18, 18, 18, 0.28);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.18em;
  color: #61615E;
}

/* Boxless variant. Padding and border are removed outright, so the element's edge
   is its text edge — which puts the date flush with the *outer* edge of the box
   above it rather than with the text inside that box. */
.cred__tag--bare {
  border: 0;
  padding: 0;
  background: none;
}

/* The gold wash darkens the backdrop enough that the design's #8A6A18 dropped to
   4.4:1 here, and 4.1:1 over the cell's hover tint — so the text is a shade
   deeper, clearing 4.5:1 against both. */
.cred__tag--priv {
  background: rgba(176, 138, 52, 0.14);
  border-color: rgba(176, 138, 52, 0.55);
  color: #7F6426;
}

/* Single column: a dash between every pair, none against the outer frame.
   These selectors match the two-up rules' specificity so source order decides. */
@media (max-width: 720px) {
  .cred__grid { grid-template-columns: 1fr; }
  .cred__cell:nth-child(n) {
    border-right: 0;
    border-bottom: 1px dashed rgba(18, 18, 18, 0.28);
  }
  .cred__cell:last-child { border-bottom: 0; }
}

/* ---------- flight log ---------- */

.log {
  position: relative;
  background: var(--ink-deep);
  overflow: hidden;
  scroll-margin-top: 70px;
}

/* The globe layer sits under the shell (z 2 below); pointer-events off so the
   full-bleed box never intercepts board taps. */
.log__globe {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.log__globe canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Fade-in on activation. The base layer ships fully visible; script.js adds
   --fade only when it is about to drive the animation (never under reduced
   motion, whose one static frame must not be hidden), then toggles is-live as
   the section enters and leaves the viewport, so the layer breathes in each
   time it arrives on screen. */
/* Each state's own transition property governs the trip INTO that state: the
   fade-in waits 0.5s after the section arrives, then breathes in over 2.6s;
   the fade-out keeps its quicker 1.6s with no delay. */
.log__globe--fade { opacity: 0; transition: opacity 1.6s ease; }
.log__globe--fade.is-live { opacity: 1; transition: opacity 2.6s ease 0.5s; }

/* Three washes, bottom-up: a vignette returning every edge to the section's own
   background, so the animation dissolves into it rather than ending at a line; a
   uniform dim so the globe reads as texture, not subject; and heavier bands across
   the top and foot, where the eyebrow/heading and hint/controls sit directly on
   the canvas — under those bands the gold lines drop to near-nothing, which keeps
   the small text's measured contrast where it was before the canvas existed. */
.log__globe-overlay {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg, rgba(6, 6, 7, 0.9) 0%, rgba(6, 6, 7, 0) 30%, rgba(6, 6, 7, 0) 72%, rgba(6, 6, 7, 0.88) 100%),
    linear-gradient(rgba(6, 6, 7, 0.58), rgba(6, 6, 7, 0.58)),
    radial-gradient(115% 92% at 50% 48%, rgba(6, 6, 7, 0) 40%, rgba(6, 6, 7, 0.97) 100%);
}

/* The bottom pad was its own larger clamp, so it takes the same 0.6 factor directly. */
.log .shell {
  position: relative;
  z-index: 2;
  padding-block: var(--section-y-tight) calc(clamp(72px, 11vw, 120px) * 0.6);
}
.log .section-title { margin-bottom: 54px; max-width: 700px; }

.board-wrap {
  width: 100%;
  overflow: hidden;
  display: flex;
  justify-content: center;
  align-items: flex-start;
}

.board {
  width: max-content;
  padding: 18px 20px 22px;
  background: #03080F;
  border: 1px solid #1B2A42;
  border-radius: 12px;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
  transform-origin: top center;
}

.board__head {
  display: flex;
  justify-content: space-between;
  gap: 24px;
  padding: 2px 4px 14px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.26em;
  color: var(--gold);
}

/* Was #5E6F8A — 3.9:1 on the board face, under the 4.5:1 minimum. */
.board__head span + span { color: #677A98; }

.board__row {
  display: flex;
  gap: 3px;
  margin-top: 3px;
  border-radius: 4px;
  cursor: pointer;
  transition: filter 0.18s;
}

.board__row:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.board__tile {
  display: block;
  width: 25px;
  height: 36px;
  line-height: 36px;
  text-align: center;
  font-family: var(--mono);
  font-size: 17px;
  font-weight: 500;
  border-radius: 3px;
  color: #EDF1F7;
  background: linear-gradient(180deg, #101c30 0%, #0a1424 47%, #03080f 50%, #091220 53%, #060e1b 100%);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
}

.board__hint {
  margin-top: 22px;
  text-align: center;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  color: var(--muted-4);
}

/* WCAG 2.2.2 — the multi-page countries advance on a timer, so the user needs a way
   to stop it, and a way to reach the other pages once it is stopped. Hidden entirely
   until a country with more than one page is on the board. */
.board__controls {
  display: none;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 14px;
  margin-top: 16px;
}

.board__controls.is-active { display: flex; }

.board__btn {
  padding: 9px 16px;
  min-height: 34px;
  background: none;
  /* 0.60 alpha clears the 3:1 boundary contrast these buttons need (WCAG 1.4.11). */
  border: 1px solid rgba(212, 169, 79, 0.6);
  color: var(--gold);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  cursor: pointer;
  transition: background 0.2s, color 0.2s, border-color 0.2s;
}

.board__btn:hover {
  background: var(--gold);
  color: var(--ink);
}

.board__btn[disabled] {
  opacity: 0.45;
  cursor: default;
}

.board__btn[disabled]:hover {
  background: none;
  color: var(--gold);
}

.board__page {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  color: var(--muted-4);
  min-width: 84px;
  text-align: center;
}

/* ---------- contact + footer ---------- */

/* The contact panel is pinned to the bottom of the viewport and sits *under* the
   scrolling content. main carries an opaque background and a bottom margin equal to
   the panel's height (set in JS), so the last section slides up off it like a blind. */
main {
  position: relative;
  z-index: 1;
  background: var(--ink);
}

.contact {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 0;
  overflow: hidden;
}

.contact__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.contact__scrim { position: absolute; inset: 0; background: rgba(6, 6, 7, 0.88); }

.contact .shell {
  position: relative;
  z-index: 2;
  padding-block: 64px;
  text-align: center;
}

.contact .eyebrow { margin-bottom: 16px; }

.contact .section-title {
  margin: 0 auto 22px;
  max-width: 720px;
  font-size: clamp(30px, 3.2vw, 42px);
  line-height: 1.08;
}

.contact__email {
  display: inline-block;
  padding-bottom: 4px;
  font-family: var(--serif);
  font-size: clamp(22px, 2.4vw, 28px);
  font-weight: 600;
  border-bottom: 1px solid rgba(212, 169, 79, 0.5);
}

.contact__email:hover { border-color: var(--gold-bright); }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
