: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(52px, 7vw, 104px);
}

* { 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);
}

/* Now a link to the page top. The explicit hover keeps the global a:hover gold
   from winning by specificity — the brand brightens like the nav links do, but
   stays deliberate rather than a cascade accident. */
.nav__brand:hover { color: var(--gold-bright); }

.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(68px, 9vw, 136px);
  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(136px, 18vw, 272px);
    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;
}

/* The rail and dots default to their FINISHED state — fully gold — because that
   is what a visitor sees when script.js never runs: nothing would otherwise set
   --fill or .is-lit, leaving a white rail and white dots that look unfinished
   rather than un-animated. The scroll-linked behaviour is gated behind
   .is-animated, which the script adds before its first measurement, so the
   animation still starts from empty for everyone who can actually see it run.
   Gold measures 8.98:1 on --ink and 9.25:1 on --ink-deep. */
.timeline::after {
  content: "";
  position: absolute;
  left: -1px;
  top: 0;
  width: 1px;
  height: 100%;
  background: var(--gold);
}

.timeline.is-animated::after { height: var(--fill, 0px); }

.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(--gold);
  box-shadow: 0 0 0 4px rgba(212, 169, 79, 0.16);
  transition: background 0.45s ease, box-shadow 0.45s ease;
}

/* Under script control the dots start unlit and light as the fill reaches them.
   The .is-lit rule carries one more class, so it wins whatever the source order. */
.timeline.is-animated .tl-item::before {
  background: var(--cream);
  box-shadow: 0 0 0 0 rgba(212, 169, 79, 0);
}

.timeline.is-animated .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 ---------- */

/* position: relative anchors the full-bleed footage layer; the section's own
   gradient is also the "black" the footage choreography rests on between clips. */
.aircraft {
  position: relative;
  background: linear-gradient(180deg, var(--ink) 0%, #101012 200px, #101012 100%);
  scroll-margin-top: 70px;
}

/* ---------- aircraft background footage ----------
   A video layer under the whole section, driven by script.js: the selected
   aircraft's b-roll plays behind the selector, dimmed by the overlay below.
   Fade-out and fade-in carry different durations, so each state's own
   transition property governs the trip INTO it — the same pattern as the
   globe's fade. The overlay lights together with the footage (.is-live), so
   with no video showing — other aircraft, no-JS, reduced motion — the section
   keeps its exact resting design. */
.air__media {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

/* The uniform dim lives on the footage itself rather than in a layer above it,
   so it applies at every opacity the fade passes through. As part of the
   overlay it faded in WITH the video, which left the picture only half-dimmed
   — and conspicuously bright — at the midpoint of the 5s entrance. It also
   cannot be a persistent layer instead: nothing clips such a layer to the
   footage, so at rest it would darken the section's own background to #070708
   while the experience section above stayed #0B0B0C, reopening the very seam
   the vignette exists to close (and doing so even with scripts blocked).

   Worst-case pure-white frame becomes #3B3B3B — and that frame is real, not
   theoretical: the 777 clip's brightest pixel is 254, the fuselage, and it
   passes right behind the spec panel. Measured there: --gold 5.11:1,
   --muted-2 4.53:1, --body 9.21:1, cream 10.09:1.

   --muted-2 at 4.53:1 is the binding constraint on this value. Raising the
   footage further darkens nothing but costs legibility, which is why phones —
   where the picture reads dimmer — get their own brighter value at the foot of
   this file, together with the two label colours that need to move with it. */
.air__media video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: brightness(0.23);
  opacity: 0;
  transition: opacity 0.5s ease;   /* the fade-out */
}

.air__media video.is-showing {
  opacity: 1;
  transition: opacity 4s ease;     /* the fade-in */
}

/* Edge treatment only — the uniform dim now rides on the footage above. A
   tight vignette hugging all four edges: fully the section's own ground AT
   each edge and gone within an even 120px on every side, so almost none of the
   picture is given up. The top fades from the experience section's --ink and
   the other three from this section's #101012, each matching the colour the
   edge actually meets, so no boundary between sections (or against the
   section's own ground) can be distinguished while footage plays. Corners
   double up naturally where the bands overlap.

   This layer DOES still fade with the footage, and should: it exists to blend
   the picture's edges into the section, so with no picture there is nothing to
   blend, and holding it would darken the section's own edges at rest. */
.air__media-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  opacity: 0;
  transition: opacity 0.5s ease;
  /* The bottom band carries a mid-stop the others don't: a plain 120px fade
     only reaches real opacity in its last few pixels, and over footage that is
     bright at the frame's foot it read as no vignette at all (over dark tarmac
     any band is invisible — nothing to darken). Holding ~62% dim through the
     first 52px makes the bottom read like the top does, at the same 120px
     reach. */
  background:
    linear-gradient(180deg, #0B0B0C 0%, rgba(11, 11, 12, 0) 120px),
    linear-gradient(0deg, #101012 0%, rgba(16, 16, 18, 0.62) 52px, rgba(16, 16, 18, 0) 120px),
    linear-gradient(90deg, #101012 0%, rgba(16, 16, 18, 0) 120px),
    linear-gradient(270deg, #101012 0%, rgba(16, 16, 18, 0) 120px);
}

/* A fixed band is a fixed band, but the picture it frames is not. 120px a side
   is 16% of a desktop's width and 31% of an iPad's — but 64% of a phone's,
   measured, at which point it stops reading as an edge treatment and becomes a
   heavy frame with a slot of video inside it. Phones get a much shorter fade,
   keeping the four-sided uniformity, which puts them at 24% horizontally and
   13% vertically — lighter than the desktop treatment in both directions.

   Every stop is scaled by the same 0.375, so the bottom band keeps its
   mid-stop and goes on reading like the top rather than flattening into the
   plain fade the other three use.

   The cut is at 640px, not the 900px the menu uses: iPads (744 and 768 CSS px)
   look right at the full 120px and must keep it. And this sits immediately
   below the rule it overrides on purpose — a media query adds no specificity,
   so source order is the only thing separating them. */
@media (max-width: 640px) {
  .air__media-overlay {
    background:
      linear-gradient(180deg, #0B0B0C 0%, rgba(11, 11, 12, 0) 45px),
      linear-gradient(0deg, #101012 0%, rgba(16, 16, 18, 0.62) 20px, rgba(16, 16, 18, 0) 45px),
      linear-gradient(90deg, #101012 0%, rgba(16, 16, 18, 0) 45px),
      linear-gradient(270deg, #101012 0%, rgba(16, 16, 18, 0) 45px);
  }

  /* The same footage reads darker on a phone — a small, bright screen held at
     arm's length, and a picture downscaled to 0.39x rather than stretched
     across a desk. Only phones get the lift; desktop and tablet keep 0.23,
     where they look right.

     The two label colours have to travel with it. Against the brighter
     worst-case frame (#474747, and the 777's fuselage really does hit it)
     --gold falls to 4.24:1 and --muted-2 to 3.76:1, both under the 4.5:1 floor
     for text this small. One step up each restores the margin: --gold-bright
     5.76:1 and --muted 5.59:1. Everything else already clears it — idle tabs
     4.72:1, values 7.64:1, the aircraft name 8.37:1.

     Both are scoped to .air__spot — that is where footage sits behind the text,
     and it is also what makes them bite: this block sits ABOVE the base rules
     it overrides, and a media query carries no specificity of its own, so a
     bare .air__label here would simply lose to the one declared further down
     the file. The extra class settles it wherever the rules happen to sit.
     (The script-less list keeps the original gold; no video is under it.) */
  .air__media video { filter: brightness(0.28); }

  .air__spot .air__specs dt { color: var(--gold-bright); }
  .air__spot .air__label { color: var(--muted); }
}

.air__media.is-live .air__media-overlay {
  opacity: 1;
  transition: opacity 2s ease;     /* rides the fade-in it dims */
}

/* Decorative motion: reduced-motion visitors never get the layer at all.
   script.js already declines to build the videos; this is the belt to that. */
@media (prefers-reduced-motion: reduce) {
  .air__media { display: none; }
}

/* 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. */
/* Positioned above the footage layer, like .log .shell above the globe. */
.aircraft .shell { position: relative; z-index: 2; 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;
}

/* Positioned so the bubble below can be placed from the buttons' own offsets. */
.air__menu {
  position: relative;
  display: flex;
  flex-direction: column;
  padding-right: 28px;
  border-right: 1px solid rgba(245, 243, 236, 0.15);
}

/* The filter is adapted from liquid-glass-react's standard mode (MIT; full
   notice in THIRD_PARTY_NOTICES.txt). It bends three separately displaced RGB
   copies of the glass face, screens them back together, and confines the colour
   split to the capsule's edge while leaving the centre clean. The two masked
   layers below are the source's screen/overlay highlights; their diagonal fades
   make the light uneven without painting a solid border.

   Its width follows the complete selected button, filling the single desktop
   column or one responsive grid column at every breakpoint. */
.air__glass-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
  pointer-events: none;
}

.air__bubble {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
  border-radius: 16px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
  isolation: isolate;
  pointer-events: none;
  opacity: 0;
  z-index: 0;
}

.air__bubble-warp,
.air__bubble-edge {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
}

/* Source component's .glass__warp layer: keep the face translucent and the
   selected label (which is a separate element above it) perfectly sharp. */
.air__bubble-warp {
  overflow: hidden;
  background: rgba(245, 243, 236, 0.035);
  filter: url(#air-glass-refraction);
  backdrop-filter: blur(6px) saturate(140%);
  -webkit-backdrop-filter: blur(6px) saturate(140%);
}

/* Two versions of the same tapered highlight are deliberately blended in
   different modes, as in the source: screen supplies the gathered light and
   overlay lets it inherit the tone underneath. */
.air__bubble-edge {
  padding: 1.5px;
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  box-shadow:
    inset 0 0 0 0.5px rgba(212, 169, 79, 0.62),
    inset 0 1px 3px rgba(212, 169, 79, 0.25),
    0 1px 4px rgba(0, 0, 0, 0.35);
}

.air__bubble-edge--screen {
  opacity: 0.2;
  mix-blend-mode: screen;
  background: linear-gradient(135deg,
    rgba(212, 169, 79, 0) 0%,
    rgba(212, 169, 79, 0.12) 33%,
    rgba(212, 169, 79, 0.4) 66%,
    rgba(212, 169, 79, 0) 100%);
}

.air__bubble-edge--overlay {
  mix-blend-mode: overlay;
  background: linear-gradient(135deg,
    rgba(212, 169, 79, 0) 0%,
    rgba(212, 169, 79, 0.32) 33%,
    rgba(212, 169, 79, 0.6) 66%,
    rgba(212, 169, 79, 0) 100%);
}

/* Added after the first placement is flushed, so the bubble fades in where it
   belongs instead of flying out of the corner on load. */
.air__bubble--ready {
  opacity: 1;
  transition: transform 0.42s cubic-bezier(0.4, 0, 0.2, 1),
              height 0.42s cubic-bezier(0.4, 0, 0.2, 1),
              opacity 0.3s ease;
}

.air__item {
  position: relative;
  padding: 12px 20px;
  border: 0;
  border-radius: 16px;
  background: none;
  text-align: left;
  font-family: var(--serif);
  font-size: 22px;
  font-weight: 400;
  color: #B9B9B4;
  cursor: pointer;
  transition: color 0.28s ease;
}

.air__item:hover { color: var(--cream); }

/* The glass is transparent, so the label keeps its own colour rather than
   flipping to ink against a solid fill. --gold-bright is the lit tone, which is
   what the refracted edge is doing too; 8.60:1 on the glass face. */
.air__item[aria-selected="true"] {
  color: var(--gold-bright);
  font-weight: 400;
}

.air__item-label-anchor,
.air__item-label {
  display: inline-block;
}

/* Keep every label in a foreground layer above the sibling glass host. This
   cannot be tied to a width breakpoint: landscape phones can expose a CSS
   viewport wider than the responsive menu breakpoint while still needing the
   selected text protected from backdrop blur. The button itself deliberately
   stays out of the stacking order, and pointer input passes through this label
   layer so the glass keeps its hover interaction. */
.air__item-label-anchor {
  position: relative;
  z-index: 3;
  pointer-events: none;
}

.air__item-label {
  transform-origin: center;
  transition: transform 0.2s ease-out;
  will-change: transform;
}

/* React hosts the complete liquid-glass-react component in this measured box. The
   host performs the tab-to-tab slide; the component keeps its own local
   translate/scale transform for mouse elasticity, hover and pressed states.
   Its z-index remains below the foreground labels in every orientation. */
/* The host owns the glass's backdrop blur, and must stay transform-free for it
   to work: the component inside positions itself with translate(-50%, ...) —
   its elasticity rides the same transform — and in Chromium any transformed
   ancestor becomes a backdrop root, cutting a descendant's backdrop-filter off
   from everything behind it. That is why the component's own inline blur never
   showed: it could only sample its own transparent subtree. The host sits
   outside every transform (placed by plain left/top from React), so ITS
   backdrop is the real page — the footage included. blur(7.2px) saturate(130%)
   reproduces the component's intended values: (4 + blurAmount 0.1 × 32)px and
   saturation 130. The selected label stays sharp because its anchor paints at
   z-index 3, above this host. */
.air__liquid-host {
  position: absolute;
  z-index: 2;
  border-radius: 16px;
  backdrop-filter: blur(7.2px) saturate(130%);
  -webkit-backdrop-filter: blur(7.2px) saturate(130%);
}

.air__liquid-host.is-ready {
  transition:
    left 0.42s cubic-bezier(0.4, 0, 0.2, 1),
    top 0.42s cubic-bezier(0.4, 0, 0.2, 1);
}

/* The published component uses a handful of Tailwind utility names in its
   generated markup. These scoped equivalents provide only the declarations the
   component requires; the site does not add Tailwind or a second CSS runtime. */
.air__liquid-host .pointer-events-none { pointer-events: none; }
.air__liquid-host .opacity-0 { opacity: 0; }
.air__liquid-host .opacity-20 { opacity: 0.2; }
.air__liquid-host .opacity-100 { opacity: 1; }
.air__liquid-host .mix-blend-overlay { mix-blend-mode: overlay; }
.air__liquid-host .bg-black { background-color: #000; }
.air__liquid-host .cursor-pointer { cursor: pointer; }
.air__liquid-host .text-white { color: #fff; }

.air__liquid-glass {
  display: block;
  border-radius: inherit;
}

.air__liquid-host .glass {
  width: 100%;
  height: 100%;
  justify-content: center;
  padding: 0 !important;
}

/* The component gives its decorative layers `transition: 0.2s ease-out`, whose
   shorthand sets transition-property to ALL — including the width and height
   it derives from its own measured size. It starts from a hardcoded
   270x69 default and measures the real box afterwards, so those layers ANIMATE
   from 270x69 to the pill's size, and until that transition finishes they keep
   the wrong geometry. Observed in Brave: two CSSTransitions on width and
   height, playState "running" but currentTime pinned at 0, leaving the rim
   drawn at 270x69 over a blur at 211x51. It reads as an outline and a blur
   that disagree, and the gap changes with orientation because the button width
   changes underneath it.

   Box geometry must never animate here — these layers exist to sit exactly on
   the pill. Only the elastic transform and the opacity fades should move, so
   the property list is narrowed to those and the library's 0.2s duration is
   left to govern them. */
.air__liquid-host > * {
  transition-property: transform, opacity !important;
}

/* iOS/iPadOS can rasterize a 1.5px masked edge more faintly than desktop
   browsers, especially at high device-pixel ratios. Touch-first devices get a
   wider refractive band and slightly stronger inset light while retaining the
   same uneven gradients—this is not converted into a uniform solid outline.
   The 0.78 gold composites to #A98742 on #101012, measured at 5.64:1. */
@media (hover: none), (pointer: coarse), (any-pointer: coarse) {
  .air__liquid-host > span,
  .air__bubble-edge {
    padding: 2.5px !important;
    box-shadow:
      inset 0 0 0 0.75px rgba(212, 169, 79, 0.78),
      inset 0 1px 4px rgba(212, 169, 79, 0.38),
      0 1px 5px rgba(0, 0, 0, 0.38) !important;
  }
}

/* The bubble is painted, not outlined, so focus needs its own ring to stay
   visible on both the selected and unselected items. */
.air__item:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 3px;
}

.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;
  /* Was --muted-3, which cannot survive over footage: 3.64:1 against a
     worst-case bright frame. --muted-2 reads 4.53:1 there — the tightest
     margin in the section, and the reason brightness cannot rise past 0.23
     without this colour moving too (see the phone override at the foot). */
  color: var(--muted-2);
}

.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: 12px 10px;
    text-align: center;
  }

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

  .air__specs { margin-inline: auto; }
}

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

/* ---------- aircraft without the selector ----------
   The static list carries all six aircraft and every spec, and is what a visitor
   sees whenever script.js is blocked, fails to load or throws. script.js marks
   the section .is-interactive once it has parsed the list and can drive the
   selector; only then do the two swap. Both toggles carry more classes than the
   base rules they override, so source order cannot defeat them.

   Entries reuse .air__specs verbatim, so each one is the same object as the
   spotlight panel the selector would have shown — the fallback is six of those
   stacked rather than a different component. On the section's #101012 face:
   cream names 17.12:1, gold labels 8.68:1, --body values 15.62:1. */

.aircraft:not(.is-interactive) .air__grid { display: none; }
.aircraft.is-interactive .air__list { display: none; }

/* Only the type and its class are shown. Role and crew position stay in the
   markup because script.js reads them from here to fill the spotlight panel —
   they are the same single copy of the data — but the script-less view is meant
   to be a summary, not the whole table, so those two rows are not displayed. */
.air__list [data-row="role"],
.air__list [data-row="pos"] { display: none; }

.air__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: clamp(24px, 3vw, 38px);
  max-width: 920px;
  margin: 0 auto;
  padding: 0;
  list-style: none;
}

.air__entry-name {
  margin: 0 0 16px;
  font-family: var(--serif);
  font-size: 30px;
  font-weight: 600;
  line-height: 1.1;
  color: var(--cream);
}

/* ---------- 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%),
    /* Dropped from 0.58: what survives the dim rose from 42% to 75% — the globe
       reads clearly now rather than as a whisper. The bands above and the vignette
       below keep their strengths, so text areas and edges are unchanged. */
    linear-gradient(rgba(6, 6, 7, 0.25), rgba(6, 6, 7, 0.25)),
    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;
}

/* ---------- destinations without the board ----------
   The static list in index.html carries every destination and is what a visitor
   sees whenever script.js is blocked, fails to load or throws. script.js marks
   the section .is-interactive once it has parsed the list and can drive the
   board; only then do the two swap. Both toggles are written with :not()/.class
   compounds so they outrank the base rules above regardless of source order —
   this file has twice been bitten by a rule sitting above what it meant to
   override.

   The panel deliberately borrows the board's own face (#03080F on #1B2A42, the
   same radius and drop shadow) so the fallback reads as the same object in a
   quieter state rather than as a different component. Measured on that face:
   cream heading 18.09:1, gold count and head 9.17:1, --muted city text 12.09:1,
   the head's #677A98 4.60:1 — all clear of the 4.5:1 minimum, the last of them
   only just, so treat it as fixed. */

.log:not(.is-interactive) .board-wrap,
.log:not(.is-interactive) .board__hint { display: none; }

.log.is-interactive .destlist { display: none; }

.destlist {
  max-width: 760px;
  margin: 0 auto;
  padding: 18px 22px 8px;
  background: #03080F;
  border: 1px solid #1B2A42;
  border-radius: 12px;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
}

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

.destlist__head span + span { color: #677A98; }

.destlist__countries {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Name and count share the first row, the cities span beneath — the same
   shape as a departures manifest, which is what the board imitates. */
.destlist__country {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 2px 18px;
  padding: 15px 2px;
  border-top: 1px dashed rgba(212, 169, 79, 0.18);
}

.destlist__country:first-child { border-top: 0; }

.destlist__name {
  grid-column: 1;
  margin: 0;
  font-family: var(--serif);
  font-weight: 600;
  font-size: 25px;
  line-height: 1.15;
  color: var(--cream);
}

.destlist__count {
  grid-column: 2;
  justify-self: end;
  margin: 0;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  white-space: nowrap;
  color: var(--gold);
}

/* The city names stay in the markup because script.js reads them from here to
   build the board — they are the same single copy of the data — but the
   script-less view lists countries and their airport counts only, so the names
   themselves are not displayed. (Consequence worth knowing: hidden text carries
   no SEO weight, so those 128 city names no longer read as page content.) */
.destlist__cities { display: none; }

@media (max-width: 600px) {
  .destlist { padding: 16px 16px 6px; }
  .destlist__name { font-size: 22px; }
  .destlist__cities { font-size: 13.5px; }
}

/* ---------- 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);
}

/* Pinning the panel and reserving space below main are one mechanism, and half of
   it lives in script.js. Pinned by default, a script-less visitor would get a panel
   parked behind an opaque main with no margin to uncover it — covered at every
   scroll position, and unreachable. So the flow layout is the default and .is-pinned
   is added by script.js at the same moment it reserves the matching height. As an
   ordinary footer the panel needs no reserved space, and #contact links resolve
   natively instead of through the click handler that would also be missing. */
.contact {
  position: relative;
  z-index: 0;
  overflow: hidden;
  scroll-margin-top: 70px;
}

.contact.is-pinned {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
}

.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;
  }
}
