@import url("./fonts.506cf7ec57.css");
@import url("./tokens.b01700bdbc.css");

/* =========================================================== foundations == */

*, *::before, *::after { box-sizing: border-box; }
html, body { overflow-x: clip; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-md);
  line-height: var(--lh-body);
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
}

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

:where(a):focus-visible,
:where(button):focus-visible,
:where([tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  /* never animated — the ring must appear instantly */
}

img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

/* ================================================================= type == */

.display {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  /* `anywhere` split designations mid-token — "Su-30MKI" wrapped as SU- / 30MKI.
     `break-word` only intervenes when a word genuinely cannot fit. */
  overflow-wrap: break-word;
  text-wrap: balance;
  min-width: 0;
  margin: 0;
}
.display--hero { font-size: var(--text-hero); }
.display--xxl  { font-size: var(--text-xxl); }
.display--lg   { font-size: clamp(1.75rem, 3.4vw, 2.75rem); }
.display--xl   { font-size: var(--text-xl); }
/* Wraps a hyphenated designation so it cannot break at the hyphen. */
.nb { white-space: nowrap; }

/* The micro-label. Every button, nav item and tag on the reference site is
   this one style, and the contrast against the display face is the whole feel. */
.label {
  font-family: var(--font-mono);
  font-size: var(--text-label);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  line-height: 1.8;
  margin: 0;
}
.label--dim { color: var(--ink-45); }

.prose {
  max-width: var(--measure);
  font-size: var(--text-lg);
  line-height: 1.55;
  letter-spacing: -0.005em;
  margin: 0;
}
.prose + .prose { margin-top: 1em; }

/* Visually hidden, still announced. The hangar needs one real h1 for the page
   while each aircraft panel carries an h2 — 22 h1s (one per panel, all but one
   hidden) is 22 h1s to a screen reader and a crawler. */
.vh {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* ================================================================= chrome == */
/* The HUD.
 *
 * Measured from the references rather than guessed at: all three float their
 * chrome over the world instead of stacking it above the content. Lama Lama's
 * pill sits centre-top with the section callsign in the middle; a card rail
 * hangs top-right; a status strip runs the full width at the bottom. Zero adds
 * a corner readout and a tick-tape. Aviation gives all of it a reason to exist.
 */

.hud {
  position: fixed; inset: 0; z-index: 90;
  pointer-events: none;                 /* the world stays draggable underneath */
}
.hud a, .hud button, .hud [data-card] { pointer-events: auto; }

/* ---- the glass ----------------------------------------------------------- */
/* zero.university builds its pills from seven stacked layers on blend modes and
   never touches backdrop-filter. That is what makes them read as a material
   rather than as a blurred rectangle, and it stays cheap on a page that is
   already running WebGL. This is the two-layer version of the same idea: an
   inner-glow rim and a burn pass over a translucent fill. */
.glass {
  position: relative;
  background: color-mix(in oklab, var(--paper-raised) 95%, transparent);
  border: var(--rule) solid var(--hairline);
  border-radius: 999px;
  isolation: isolate;
  overflow: hidden;
}
/* No backdrop-filter here. These pills are position:fixed and always on
   screen, so the compositor has to re-sample everything behind them on every
   single frame of every scroll — the most expensive thing on the page by a
   distance. A near-opaque fill is indistinguishable at this alpha and costs
   nothing to scroll past. */
.glass::before,
.glass::after { content: ""; position: absolute; inset: 0; pointer-events: none; }
.glass::before {                        /* top-edge light, bottom-edge shade */
  background: linear-gradient(180deg, #00000000 0%, #00000000 55%, #00000014 100%);
  mix-blend-mode: multiply;
}
.glass::after {                         /* the burn pass — gives it density */
  background: radial-gradient(120% 160% at 50% -30%, #00000000 38%, #0000000d 100%);
  mix-blend-mode: multiply;
  opacity: 0.7;
}
.glass > * { position: relative; z-index: 1; }

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

.masthead {
  position: absolute; top: var(--space-md); left: 50%;
  translate: -50% 0;
  max-width: calc(100vw - var(--page-x) * 2);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: var(--space-lg);
  padding: var(--space-sm) var(--space-lg);
  color: var(--ink);
  transition: translate var(--dur-md) var(--ease-out);
}
.masthead__mark {
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  font-size: 0.8125rem; letter-spacing: 0.02em; line-height: 1;
  white-space: nowrap;
}
/* The contextual slot. Lama Lama gives this the widest share of the pill and
   lets it rewrite itself per section; it is the part that makes the nav feel
   alive rather than decorative. */
.masthead__slot {
  justify-self: center;
  font-family: var(--font-mono); font-size: var(--text-label);
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--ink-70);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  min-width: 12ch; text-align: center;
  font-variant-ligatures: none;         /* keep decode glyphs monospaced */
}
.masthead__nav { display: flex; gap: var(--space-md); }
.masthead__nav a { position: relative; white-space: nowrap; }
.masthead__nav a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -3px; height: 1px;
  background: currentColor; transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur-sm) var(--ease-out);
}
.masthead__nav a:hover::after,
.masthead__nav a[aria-current="page"]::after { transform: scaleX(1); }

/* ---- right-hand rail ----------------------------------------------------- */
/* An introduction, not a fixture. It says who made this, then fades out of the
   way on scroll so the photographs get the whole page. Only the contact pill
   persists. Reserving a 176px gutter for it down every page was costing the
   work more room than the greeting was worth. */

.rail {
  position: absolute; top: var(--space-md); right: var(--page-x);
  width: 200px;
  display: grid; gap: var(--space-sm); justify-items: end;
  text-align: right;
}
.rail__intro {
  display: grid; gap: var(--space-2xs);
  transition: opacity 420ms var(--ease-out), transform 420ms var(--ease-out);
}
.rail__intro.is-gone {
  opacity: 0;
  transform: translateY(-8px);
  pointer-events: none;
}
.rail__name {
  font-family: var(--font-display); font-weight: 700;
  font-size: var(--text-sm); letter-spacing: -0.01em;
}
.rail__bio {
  font-size: var(--text-sm); line-height: 1.35; color: var(--ink-45);
  max-width: 24ch;
}
.rail__cta {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  padding: var(--space-xs) var(--space-md);
  color: var(--ink);
  transition: transform var(--dur-sm) var(--ease-out);
}
.rail__cta:hover { transform: translateY(-1px); }
/* A plane, banked slightly, rather than a status dot — the dot read as a
   recording indicator, which is not what this is. */
.rail__plane {
  width: 15px; height: 15px; flex: none;
  color: var(--accent);
  rotate: -20deg;
  transition: rotate var(--dur-md) var(--ease-out),
              translate var(--dur-md) var(--ease-out);
}
.rail__cta:hover .rail__plane { rotate: 0deg; translate: 2px -1px; }

/* ---- bottom status strip ------------------------------------------------- */
/* Lama Lama runs a live Amsterdam clock down here. Flight logs run on Zulu, so
   the honest version of the same idea is UTC. */

.status {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; gap: var(--space-lg);
  padding: var(--space-xs) var(--page-x);
  border-top: var(--rule) solid var(--hairline);
  background: var(--paper);
  font-family: var(--font-mono); font-size: var(--text-label);
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--ink-45);
}
/* A soft lead-in above the bar so content does not hard-cut against it. */
.status::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: 100%; height: 56px;
  background: linear-gradient(color-mix(in oklab, var(--paper) 0%, transparent),
                              var(--paper));
  pointer-events: none;
}
.status__spacer { flex: 1; }
.status b { font-weight: 400; color: var(--ink-70); }
.status__live {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--ink-70);
}
.status__live::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent);
  animation: pulse 2.4s var(--ease-in-out) infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.25 } }

/* ---- scroll reveal ------------------------------------------------------- */

[data-reveal] {
  opacity: 0; transform: translateY(16px);
  transition: opacity 680ms var(--ease-out), transform 680ms var(--ease-out);
}
[data-reveal].is-in { opacity: 1; transform: none; }
/* Without this the entire home page is blank when scripting is off: 21 entries
   sitting at opacity 0 waiting for an observer that will never run. A media
   query rather than a class added by JS, because a module is deferred and the
   class would land after first paint, which trades a blank page for a flash. */
@media (scripting: none) {
  [data-reveal] { opacity: 1; transform: none; transition: none; }
}

/* Pages that scroll need room for the fixed chrome at both ends. The sheet
   head owns the top clearance on its own — see .sheet__head. */
/* The strip is fixed over the bottom edge, so the last thing on the page needs
   to clear it rather than run underneath. */
body:not(.hangar) .colophon { padding-bottom: var(--space-3xl); }
body:not(.hangar) { padding-bottom: var(--space-xl); }

/* The rail floats over the page, so scrolling content has to be told to clear
   it. Lama Lama lets its cards sit on top of imagery, which reads fine over a
   photograph but clips a data table — the capture strip was losing its last
   column underneath. Only reserve the gutter where the rail is actually shown. */
@media (min-width: 861px) {
  /* No gutter reserved on the right. The rail's greeting fades on scroll and
     the contact pill is small enough to sit over a hero, so content — the
     reel above all — gets the full width of the page. */
}

/* On the hangar the roster owns the bottom edge, so the strip has no room.
   The roster already reports frame counts per airframe, which is the same
   information at better resolution. */
body.hangar .status { display: none; }

@media (max-width: 860px) {
  .rail { display: none; }             /* the pill and strip carry it alone */
}
@media (max-width: 700px) {
  /* Dropping the slot out of flow left the nav sitting in the middle 1fr
     track, which squeezed it against the pill's overflow:hidden and cut
     "Archive" and "About" off entirely. Two auto tracks instead. */
  .masthead {
    grid-template-columns: auto auto;
    gap: var(--space-sm); padding: var(--space-xs) var(--space-md);
  }
  .masthead__slot { display: none; }   /* the mark and links come first */
  .masthead__mark { font-size: 0.6875rem; }
  .masthead__nav { gap: var(--space-sm); }
  .status { gap: var(--space-md); }
  .status__drop { display: none; }
  /* The fixed pill needs less clearance when the page starts this narrow. */
  .sheet__head { padding-top: var(--space-4xl); }
}
@media (prefers-reduced-motion: reduce) {
  [data-reveal] { opacity: 1; transform: none; transition: none; }
  .status__live::before { animation: none; }
  .rail__body { transition: none; }
}

/* ============================================================== hangar ==== */
/* The GQ model: one fixed viewport, no page scroll, spatial selection. */

body.hangar { overflow: hidden; height: 100dvh; }

/* Flex column, not grid. With a grid the single `1fr` track was resolving to
   the roster's min-content (21 tiles, about 2800px) and overflowing a 534px
   container, dragging the viewer off-screen. A column flexbox stretches its
   children to the container width instead, so the roster scrolls inside itself. */
.hangar__stage {
  position: relative;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Holds the identity block and the viewer as siblings. */
.hangar__main {
  position: relative;
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  min-height: 0;
  width: 100%;
  overflow: hidden;
}

.viewer {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 0; min-width: 0;
  /* pan-y, not none. The turntable only ever reads the horizontal axis: the
     drag handler uses e.clientX and nothing else. Claiming both axes meant a
     finger anywhere on the model swallowed vertical swipes too, so on a phone
     the aircraft pages could not be scrolled past the viewer at all. The page
     keeps the vertical axis; horizontal still rotates the aircraft. */
  touch-action: pan-y;
  cursor: grab;
}
.viewer:active,
.viewer.is-dragging { cursor: grabbing; }

/* The turntable frames stack; exactly one is shown. */
.viewer__frames {
  position: relative;
  /* Height is stated outright, not left to aspect-ratio. The only child is an
     absolutely-positioned canvas, so the box has no in-flow content to give it
     height, and a zero-height box means a zero-size canvas that renders to
     nothing while the animation loop happily keeps ticking. */
  width: min(72vw, 66vh, 780px);
  height: min(72vw, 66vh, 780px);
  min-width: 280px;
  min-height: 280px;
  color: var(--ink);
}
body.hangar .viewer__frames {
  width: min(52vw, 68vh, 720px);
  height: min(52vw, 68vh, 720px);
}
.viewer__frames canvas {
  position: absolute; inset: 0;
  /* Never let the frame blender's opacity handling reach the live canvas. */
  opacity: 1 !important;
  visibility: visible !important;
}

.viewer__frames svg,
.viewer__frames img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; visibility: hidden;
  /* opacity is driven per-frame by the blender in turntable.js; no transition
     here or it would fight the cross-dissolve */
  will-change: opacity;
}

/* A ground shadow keeps the object from floating — the single most common
   reason a web turntable reads as cheap (model-viewer ships shadow-intensity 0). */
.viewer__shadow {
  position: absolute; left: 50%; bottom: 12%;
  width: 46%; height: 3.2%;
  translate: -50% 0;
  background: radial-gradient(ellipse at center, #00000026 0%, #00000000 70%);
  pointer-events: none;
}

/* Drag affordance: a hairline arc that fades in on hover, no clumsy label. */
.viewer__hint {
  position: absolute; left: 50%; bottom: 7%; translate: -50% 0;
  display: flex; align-items: center; gap: var(--space-xs);
  color: var(--ink-45);
  opacity: 0; transition: opacity var(--dur-md) var(--ease-out);
}
.viewer:hover .viewer__hint,
.viewer:focus-within .viewer__hint { opacity: 1; }
.viewer.is-dragging .viewer__hint { opacity: 0; }

/* On the hangar this used to sit top-right, where it now collides with the
   contact pill and the greeting. It belongs down by the stage anyway — next to
   the thing it describes rather than up in the chrome. */
body.hangar .viewer__azimuth {
  top: auto; bottom: var(--space-xl); right: var(--page-x);
}
.viewer__azimuth {
  position: absolute; top: var(--space-xl); right: var(--page-x);
  text-align: right; color: var(--ink-45);
  font-variant-numeric: tabular-nums;
}

/* Aircraft identity, set against the model rather than over it. */
.hangar__id {
  position: absolute; z-index: 10;
  left: var(--page-x); top: 50%; translate: 0 -50%;
  width: min(46ch, 44vw);      /* long names were wrapping to three lines */
  pointer-events: none;
}
.hangar__id > * { pointer-events: auto; }
.hangar__meta { margin-top: var(--space-sm); }

/* Bottom carousel — GT7's pattern: a horizontal strip of tiles, selected one
   marked, running off the edge so the set reads as longer than the screen. */
.roster {
  position: relative; z-index: 20;
  flex: 0 0 auto;
  width: 100%;
  border-top: var(--rule) solid var(--hairline);
  /* Opaque: the hangar's roster sits over a live WebGL canvas, and a
     backdrop-filter there makes the compositor re-sample the 3D output every
     frame. Nothing shows through it anyway. */
  background: var(--paper);
}
.roster__scroll {
  display: flex; gap: 0;
  width: 100%;
  overflow-x: auto; overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.roster__scroll::-webkit-scrollbar { display: none; }

.roster__item {
  flex: 0 0 auto;
  display: grid; gap: 2px;
  padding: var(--space-sm) var(--space-md) var(--space-md);
  min-width: 132px;
  border-right: var(--rule) solid var(--hairline);
  background: none; border-top: 0; border-bottom: 0; border-left: 0;
  color: inherit; text-align: left; cursor: pointer;
  font: inherit;
  transition: background var(--dur-sm) var(--ease-out);
}
.roster__item:hover { background: var(--paper-sunk); }
.roster__item[aria-selected="true"] { background: var(--ink); color: var(--paper); }
.roster__item[aria-selected="true"] .label--dim { color: #ffffffa6; }
.roster__num { font-variant-numeric: tabular-nums; }
.roster__name {
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  font-size: 0.8125rem; letter-spacing: -0.01em; line-height: 1.15;
}

/* =========================================================== capture data == */
/* The differentiator. Exactly one site in the category surfaces EXIF and it
   does so as an unstyled debug dump. This is the designed version. */

.capture {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  border-top: var(--rule) solid var(--hairline);
  border-left: var(--rule) solid var(--hairline);
}
.capture__cell {
  padding: var(--space-sm) var(--space-md);
  border-right: var(--rule) solid var(--hairline);
  border-bottom: var(--rule) solid var(--hairline);
  min-width: 0;
}
.capture__k { color: var(--ink-45); }
.capture__v {
  font-family: var(--font-mono); font-size: var(--text-sm); font-weight: 500;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
  margin-top: var(--space-3xs); overflow-wrap: anywhere;
}

/* ============================================================ aircraft ==== */

.sheet {
  --sheet-pr: var(--page-x);            /* full-bleed children cancel this */
  padding: 0 var(--sheet-pr) var(--space-5xl) var(--page-x);
}

.sheet__head {
  padding: clamp(var(--space-4xl), 18vh, calc(var(--space-5xl) + var(--space-xl)))
           0 var(--space-2xl);
  border-bottom: var(--rule) solid var(--hairline);
}
.sheet__lede { margin-top: var(--space-xl); }

.section { padding: var(--space-4xl) 0 0; }
.section__head {
  display: grid; gap: var(--space-xs);
  padding-bottom: var(--space-lg);
  margin-bottom: var(--space-xl);
  border-bottom: var(--rule) solid var(--hairline);
}

.specs { display: grid; grid-template-columns: 1fr; gap: 0; }
.specs__row {
  display: grid; grid-template-columns: minmax(0, 11rem) minmax(0, 1fr);
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  border-bottom: var(--rule) solid var(--hairline);
}
.specs__v { font-size: var(--text-md); }

/* Grid: uniform cropped tile. Lightbox: true aspect ratio. Seven independent
   professional operations converge on exactly this split. */
.plates {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
  gap: var(--gutter);
}
.plate {
  display: grid; gap: var(--space-xs);
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left; cursor: zoom-in;
}
.plate__frame {
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background: var(--paper-sunk);
}
.plate__frame img {
  width: 100%; height: 100%; object-fit: cover;
  transition: scale var(--dur-lg) var(--ease-out);
}
.plate:hover .plate__frame img { scale: 1.02; }
.plate__foot { display: flex; justify-content: space-between; gap: var(--space-sm); }

/* =============================================================== lightbox = */

.lightbox {
  position: fixed; inset: 0; z-index: 100;
  background: var(--stage); color: var(--stage-ink);
  display: grid; grid-template-rows: auto 1fr auto;
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur-md) var(--ease-out), visibility var(--dur-md);
}
.lightbox[open], .lightbox.is-open { opacity: 1; visibility: visible; }
.lightbox__bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) var(--page-x);
  border-bottom: var(--rule) solid var(--stage-hair);
}
/* Same trap as the reel stage: with a content-sized grid row the frame grew
   past the row and spilled over the capture strip below it. Position the image
   out of flow so object-fit has a fixed box to letterbox into. */
.lightbox__stage {
  position: relative; min-height: 0; padding: var(--space-lg);
}
.lightbox__stage img {
  position: absolute; inset: var(--space-lg);
  width: calc(100% - var(--space-lg) * 2); height: calc(100% - var(--space-lg) * 2);
  object-fit: contain;
}
.lightbox__foot {
  padding: var(--space-md) var(--page-x);
  border-top: var(--rule) solid var(--stage-hair);
}
.lightbox .capture { border-color: var(--stage-hair); }
.lightbox .capture__cell { border-color: var(--stage-hair); }
.lightbox .capture__k { color: var(--ink-45); }

.iconbtn {
  background: none; border: 0; padding: var(--space-2xs) var(--space-xs);
  color: inherit; font: inherit; cursor: pointer;
}

/* ================================================================ footer == */

.colophon {
  border-top: var(--rule) solid var(--hairline);
  padding: var(--space-2xl) var(--page-x) var(--space-xl);
  display: grid; gap: var(--space-xl);
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
}

/* ============================================================ responsive == */

@media (max-width: 1024px) {
  .hangar__main { grid-template-rows: auto minmax(0, 1fr); }
  .hangar__id {
    position: static; translate: none; width: auto;
    padding: calc(var(--space-4xl)) var(--page-x) 0;
  }
  body.hangar .viewer__frames { width: min(74vw, 46vh, 520px); }
  .viewer__azimuth { top: var(--space-sm); right: var(--page-x); }
}

@media (max-width: 640px) {
  .specs__row { grid-template-columns: 1fr; gap: var(--space-3xs); }
  .hangar__id { padding-top: var(--space-4xl); }
  body.hangar .viewer__frames { width: min(80vw, 40vh, 420px); }
}

@media (prefers-reduced-motion: reduce) {
  .plate:hover .plate__frame img { scale: 1; }
  .masthead__nav a::after { transition: none; }
}

/* ====================================================== coverage ring ===== */
/* Where the photographs actually are, around the airframe. Two-fifths of the
   compass is empty, because an airshow photographer shoots from one spot beside
   the runway. Showing that is more honest, and more interesting, than hiding it. */

.coverage {
  /* Sized to the model box, not the viewer. Off the viewer it was 92vh wide on
     a tall panel, overflowing the frame and dragging the ticks off to one side. */
  position: absolute; left: 50%; top: 50%;
  translate: -50% -50%;
  width: min(72vw, 66vh, 780px);
  height: auto; aspect-ratio: 1;
  pointer-events: none;
  color: var(--ink);
}
body.hangar .coverage { width: min(52vw, 68vh, 720px); }
.coverage__ring { fill: none; stroke: var(--hairline); stroke-width: 0.6; }
.coverage__ticks line { stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; }
.coverage__needle { fill: var(--accent); }
.coverage__read {
  position: absolute; left: 50%; bottom: 1.5%;
  translate: -50% 0;
  color: var(--ink-45); text-align: center;
  font-variant-numeric: tabular-nums;
}

/* A plate lights when the turntable is pointing near the bearing it was shot
   from. Nothing is ever hidden — with 41 of 81 frames inside one 45-degree
   sector, filtering would leave the gallery empty most of the way round. */
/* The highlight is additive. Dimming the photographs to make the turntable
   feel connected got the priority backwards — the pictures are the work, and a
   gallery sitting at 42% opacity just reads as broken. Matching frames gain a
   marker; the rest are left alone at full strength. */
.plates .plate__frame::after {
  content: ""; position: absolute; inset: 0;
  outline: 0 solid var(--accent); outline-offset: 0;
  pointer-events: none;
  transition: outline-width var(--dur-md) var(--ease-out),
              outline-offset var(--dur-md) var(--ease-out);
}
.plates.is-linked .plate.is-lit .plate__frame::after {
  outline-width: 2px; outline-offset: -2px;
}
.plates.is-linked .plate.is-lit .plate__bearing {
  color: var(--accent);
}
.plate__bearing { font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) {
  .plates .plate__frame::after { transition: none; }
}

/* ================================================================== rows == */
/* Lama Lama's work list. A row is: index, name, mono tag boxes, count, a ( + )
 * marker, and then a strip of frames that runs off the right edge of the page.
 *
 * The bleed is the point. It says the row holds more than the screen shows, and
 * it turns a long uniform list into something scannable. A grid of equal cards
 * gives every aircraft the same weight; this does not.
 */

.sheet--wide { --sheet-pr: 0px; }
@media (min-width: 861px) {
  /* The rail still needs clearance, but only over the heads — the strips are
     supposed to reach the edge. */
  body:not(.hangar) .sheet--wide { --sheet-pr: 0px; }
  .sheet--wide .sheet__head,
  .sheet--wide .section__head { padding-right: calc(176px + var(--space-xl)); }
}
.section--flush { padding-top: var(--space-3xl); }

.rows { border-top: var(--rule) solid var(--hairline); }

.row {
  /* The archive is 29 rows carrying up to six frames each. Without this the
     browser lays out and paints all of them on every scroll; with it, rows
     outside the viewport cost nothing. The intrinsic size keeps the scrollbar
     honest so the page does not jump as rows come into range. */
  content-visibility: auto;
  contain-intrinsic-size: auto 118px;
  position: relative;
  display: grid;
  /* index · name · tags · count · marker · strip. The strip gets its own track
     rather than being laid over the row: absolutely positioning it buried the
     count and the ( + ) underneath the photographs. */
  grid-template-columns:
    3ch minmax(13ch, 19ch) auto auto auto minmax(0, 1fr);
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  margin-left: var(--page-x);
  border-bottom: var(--rule) solid var(--hairline);
  color: inherit;
  overflow: hidden;                     /* the strip leaves through this edge */
  transition: background var(--dur-sm) var(--ease-out);
}
.row:hover { background: color-mix(in oklab, var(--ink) 4%, transparent); }

.row__name {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1rem, 1.5vw, 1.25rem);
  letter-spacing: -0.01em; line-height: 1.15;
}
.row__tags { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
.tag {
  padding: 3px 7px;
  border: var(--rule) solid var(--hairline-mid);
  font-family: var(--font-mono); font-size: var(--text-label);
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--ink-45);
  white-space: nowrap;
}
.row:hover .tag { color: var(--ink-70); border-color: var(--hairline); }
.row__count { white-space: nowrap; }
.row__mark { text-align: right; white-space: nowrap; }  /* "( + )" must not wrap */

/* The strip fills its track and keeps going past the page edge, where the row's
   overflow clips it. Hovering eases it left so more of the row comes into view. */
.row__strip {
  display: flex; gap: var(--space-2xs);
  justify-content: flex-start;
  pointer-events: none;
  opacity: 0.32;
  transition: opacity var(--dur-md) var(--ease-out),
              translate var(--dur-lg) var(--ease-out);
}
.row__strip img {
  width: 124px; height: 82px; object-fit: cover; flex: none;
  background: var(--paper-sunk);
}
.row:hover .row__strip { opacity: 1; translate: -56px 0; will-change: translate, opacity; }

@media (max-width: 1099px) {
  /* No room to bleed, so the strip drops to its own line and scrolls. */
  .row {
    grid-template-columns: 3ch minmax(10ch, 1fr) auto 4ch;
    padding: var(--space-md) var(--page-x) var(--space-md) 0;
  }
  .row__tags { display: none; }
  .row__strip {
    grid-column: 1 / -1; opacity: 1; translate: none;
    overflow-x: auto; padding-top: var(--space-sm);
    scrollbar-width: none;
  }
  .row__strip::-webkit-scrollbar { display: none; }
  .row__strip img { width: 104px; height: 70px; }
  .row:hover .row__strip { translate: none; }
}
@media (max-width: 640px) {
  .row { grid-template-columns: 3ch 1fr auto; gap: var(--space-sm); }
  .row__mark { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .row, .row__strip { transition: none; }
  .row:hover .row__strip { translate: none; }
}

/* ================================================================== lead == */
/* zero.university does not put a title above a scene, it puts the title ON the
 * scene and lets the scene fill the viewport. With real photographs to hand
 * that is strictly better than a headline floating on an empty ground, which is
 * what this page had.
 */

.lead {
  position: relative;
  min-height: 90dvh;
  display: grid;
  grid-template-rows: 1fr auto;
  align-items: end;
  /* Out to the page edges, past the sheet's gutter. */
  margin: 0 calc(var(--sheet-pr) * -1) var(--space-3xl) calc(var(--page-x) * -1);
  padding: var(--space-5xl) var(--page-x) var(--space-2xl);
  overflow: hidden;
}
.lead .lead__media {
  position: absolute; inset: 0; z-index: 0;
  /* The full-size frame takes ~900ms over the network, and until it lands the
     lead is a black rectangle. The 500px variant is a fraction of the weight
     and arrives almost immediately, so it stands in underneath. */
  background: var(--lqip) center / cover no-repeat;
}
.lead__media img { opacity: 0; transition: opacity 420ms var(--ease-out); }
.lead__media img.is-loaded,
.lead__media img[data-loaded] { opacity: 1; }
.lead__media img { width: 100%; height: 100%; object-fit: cover; }
/* Two stops, not one: the top scrim keeps the pill legible, the bottom one
   carries the type. A single full-height wash flattens the photograph. */
.lead::after {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background:
    linear-gradient(rgb(var(--paper-rgb) / 0.58) 0%,    /* keeps the pill legible */
                    rgb(var(--paper-rgb) / 0.05) 26%,   /* photograph breathes */
                    rgb(var(--paper-rgb) / 0.24) 46%,
                    rgb(var(--paper-rgb) / 0.62) 66%,   /* the title sits here */
                    rgb(var(--paper-rgb) / 0.90) 86%,
                    rgb(var(--paper-rgb) / 1)    100%);
}
.lead > :not(.lead__media) { position: relative; z-index: 2; }
.lead__id { display: grid; gap: var(--space-sm); align-self: end; }
/* No text-shadow. A per-glyph blur behind display-sized type reads as a muddy
   halo the moment the photograph behind it is bright, and it is an expensive
   paint besides. Legibility is the scrim's job — see .lead::after, where the
   ramp is weighted to the lower half so the title always lands on a darkened
   band rather than on bare sky. */
.lead__scroll {
  justify-self: center; align-self: end;
  margin-top: var(--space-xl);
}
.lead__scroll::after {
  content: ""; display: block; width: 1px; height: 26px; margin: 8px auto 0;
  background: linear-gradient(var(--ink-45), transparent);
}
.sheet__lede--after-lead { max-width: var(--measure); margin-bottom: var(--space-2xl); }

/* Lama Lama reveals prose a line at a time rather than as a block. Each line is
   wrapped and slid up behind its own mask, so the paragraph assembles. */
.prose--lines .ln { display: block; overflow: hidden; }
.prose--lines .ln > span {
  display: block;
  transform: translateY(105%);
  transition: transform 760ms var(--ease-out);
}
.prose--lines.is-in .ln > span { transform: none; }

@media (max-width: 700px) {
  .lead { min-height: 76dvh; padding-top: var(--space-4xl); }
}
@media (prefers-reduced-motion: reduce) {
  .prose--lines .ln > span { transform: none; transition: none; }
}

/* ================================================================== tape == */
/* zero.university's tick-tape scrubber: a ruled scale sliding under a fixed
 * playhead. Here the scale is the compass and the marked ticks are bearings
 * Rushikesh actually shot from, so it reads the archive as well as the model.
 * A readout, not a control — dragging the aircraft is the control.
 */

.tape {
  position: absolute; left: 0; right: 0; top: 0;
  padding-top: var(--space-sm);
  pointer-events: none;
  z-index: 3;
}
.tape__window {
  position: relative; height: 26px; overflow: hidden;
  mask-image: linear-gradient(90deg, transparent, #000 16%, #000 84%, transparent);
}
.tape__scale { position: absolute; inset: 0; will-change: transform; }
.tape__tick {
  position: absolute; top: 0;
  width: 1px; height: 6px;
  background: var(--ink-30);
}
.tape__tick.is-major { height: 11px; background: var(--ink-45); }
/* A bearing that carries frames gets a weighted coral mark. */
.tape__tick.has-frames {
  height: 14px;
  background: var(--accent);
  opacity: var(--weight, 1);
}
.tape__tick i {
  position: absolute; top: 13px; left: 50%;
  translate: -50% 0;
  font-family: var(--font-mono); font-size: var(--text-label);
  letter-spacing: var(--track-label); font-style: normal;
  color: var(--ink-45);
}
/* The playhead is fixed at the centre; the scale moves under it. */
.tape__head {
  position: absolute; left: 50%; top: var(--space-sm);
  translate: -50% 0;
  width: 1px; height: 18px;
  background: var(--ink);
}
.tape__head::after {
  content: ""; position: absolute; top: -4px; left: 50%;
  translate: -50% 0;
  border: 4px solid transparent; border-top-color: var(--ink);
}
.tape__read {
  margin-top: var(--space-xl); text-align: center;
}
/* The tape occupies a fixed band at the top of the viewer; the stage starts
   below it rather than underneath it. */
.viewer:has(.tape) .viewer__frames,
.viewer:has(.tape) .viewer__shadow,
.viewer:has(.tape) .coverage { margin-top: var(--tape-band); }
.viewer:has(.tape) { --tape-band: 104px; }
/* The azimuth readout sat at the tape's own height and collided with its
   right-hand number. Drop it below the band. */
.viewer:has(.tape) .viewer__azimuth { top: calc(var(--tape-band) + var(--space-xl)); }
.tape__read.is-empty { color: var(--ink-30); }

@media (max-width: 700px) {
  .tape__tick i { display: none; }     /* the numbers crowd at this width */
  .tape__read { margin-top: var(--space-md); }
}

/* ================================================================== reel == */
/* The photographs, one at a time and large, with a filmstrip for random access.
 *
 * The stage shows each frame at its true aspect on a near-black ground —
 * cropping a photographer's compositions to a uniform tile is the one thing a
 * portfolio must not do. The filmstrip crops freely, because there its job is
 * recognition, not looking.
 */

.reel { padding-top: var(--space-2xl); }
.reel__head { margin-bottom: var(--space-lg); }

/* The viewport is the band the stage is centred in; the stage is the frame
   itself, sized by JS to the exact aspect of the photograph on show. Nothing
   is ever letterboxed, so there are no black bars whose width changes on every
   move, and the arrows sit against the picture rather than out in dead space. */
.reel__viewport {
  position: relative;
  display: flex; justify-content: center;
  margin-left: calc(var(--page-x) * -1);
  padding-left: var(--page-x);
}

.reel__stage {
  position: relative;
  background: var(--stage);
  overflow: hidden;
  cursor: zoom-in;
  touch-action: pan-y;                /* the page keeps the vertical axis */
  outline-offset: -2px;
  /* Set inline by JS. The transition only runs when the aspect actually
     changes, so same-shape frames do not wobble. */
  transition: width 420ms var(--ease-out), height 420ms var(--ease-out);
}
.reel__stage.is-grabbing { cursor: grabbing; }

/* Two stacked layers. The incoming frame is decoded before it is faded up, so
   the stage is never blank between frames. */
.reel__layer {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;                  /* stage matches the aspect, so no crop */
  opacity: 0;
  transition: opacity 460ms var(--ease-out);
  user-select: none; -webkit-user-drag: none;
  pointer-events: none;
}
.reel__layer.is-on { opacity: 1; }

.reel__nav {
  position: absolute; top: 50%; translate: 0 -50%;
  width: 46px; height: 46px;
  display: grid; place-items: center;
  border: var(--rule) solid var(--hairline-mid);
  border-radius: 50%;
  background: color-mix(in oklab, var(--paper-raised) 82%, transparent);
  color: var(--ink);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--dur-sm) var(--ease-out),
              background var(--dur-sm) var(--ease-out);
  z-index: 2;
}

.reel__nav--prev { left: var(--space-lg); }
.reel__nav--next { right: var(--space-lg); }
.reel__viewport:hover .reel__nav,
.reel__nav:focus-visible { opacity: 1; }
.reel__nav:hover { background: var(--paper-raised); }

.reel__expand {
  position: absolute; right: var(--space-md); bottom: var(--space-md);
  /* This label sits on the photograph, not on the page, so the page's ground
     cannot be relied on. Black text on a dark frame vanishes and a glow around
     it was rejected once already. It gets its own white chip instead: the text
     stays black, and it is legible over any frame. */
  color: var(--ink);
  background: var(--paper);
  padding: 4px 8px;
  opacity: 0;
  transition: opacity var(--dur-sm) var(--ease-out);
  pointer-events: none;
  z-index: 2;
}
.reel__stage:hover .reel__expand { opacity: 1; }

.reel__bar {
  display: flex; align-items: baseline; gap: var(--space-lg);
  padding: var(--space-sm) 0;
  margin-top: var(--space-md);
  border-bottom: var(--rule) solid var(--hairline);
}
.reel__index { font-variant-numeric: tabular-nums; flex: none; }
.reel__meta  { font-variant-numeric: tabular-nums; }

/* ---- filmstrip ----------------------------------------------------------- */

.reel__film {
  display: flex; gap: var(--space-xs);
  padding: var(--space-md) 0;
  overflow-x: auto;
  scrollbar-width: none;
  scroll-snap-type: x proximity;
  overscroll-behavior-x: contain;
}
.reel__film::-webkit-scrollbar { display: none; }

/* The plate buttons become thumbnails here. They stay in the DOM as plates so
   link.js can still light the ones shot near the turntable's bearing. */
.reel__film .plate {
  flex: 0 0 auto;
  width: 118px;
  gap: 0;
  scroll-snap-align: center;
  cursor: pointer;
}
.reel__film .plate__frame {
  aspect-ratio: 3 / 2;
  outline: var(--rule) solid transparent;
  outline-offset: 2px;
  transition: outline-color var(--dur-sm) var(--ease-out),
              opacity var(--dur-sm) var(--ease-out);
  opacity: 0.5;
}
.reel__film .plate:hover .plate__frame { opacity: 0.85; }
.reel__film .plate.is-current .plate__frame {
  opacity: 1;
  outline-color: var(--ink);
}
.reel__film .plate__foot { display: none; }   /* the bar carries the metadata */

@media (max-width: 700px) {
  .reel__nav { opacity: 1; width: 38px; height: 38px; }
  .reel__nav--prev { left: var(--space-sm); }
  .reel__nav--next { right: var(--space-sm); }
  .reel__expand { display: none; }
  .reel__bar { flex-direction: column; gap: var(--space-2xs); }
  .reel__film .plate { width: 92px; }
}
@media (prefers-reduced-motion: reduce) {
  .reel__stage img, .reel__nav, .reel__expand,
  .reel__film .plate__frame { transition: none; }
}

/* =============================================================== kinetic == */
/* Type that moves. See src/js/kinetic.js for why these two and not others. */

/* ---- the band ------------------------------------------------------------ */

.band {
  --stretch: 0;
  position: relative;
  overflow: hidden;
  margin: var(--space-4xl) calc(var(--page-x) * -1) 0;
  padding: var(--space-lg) 0;
  border-top: var(--rule) solid var(--hairline);
  border-bottom: var(--rule) solid var(--hairline);
  user-select: none;
}
.band__track {
  display: flex;
  width: max-content;
  will-change: transform;
  /* Two identical runs sit side by side, so translating the track by exactly
     half its width lands the second run where the first began — seamless at
     any width, with nothing to measure. Runs on the compositor, so it cannot
     be stalled by a busy main thread or a throttled rAF. */
  animation: band-run var(--speed, 34s) linear infinite;
  animation-play-state: var(--play, running);
}
@keyframes band-run { to { transform: translate3d(-50%, 0, 0); } }
.band__run {
  display: flex; flex: none;
  font-family: var(--font-display);
  font-weight: 700; text-transform: uppercase;
  font-size: clamp(2rem, 7vw, 6rem);
  line-height: 1;
  letter-spacing: calc(-0.02em + var(--stretch) * 0.06em);
  transition: letter-spacing 220ms var(--ease-out);
  color: var(--ink);
  white-space: nowrap;
}
.band__run > span { padding-right: 0.32em; }
/* The separator carries the accent, so the line reads as a repeating unit
   rather than one long unbroken word. */
.band__sep { color: var(--accent); }

/* ---- split headings ------------------------------------------------------ */

.kt__word { display: inline-block; white-space: nowrap; }
.kt__char {
  display: inline-block;
  overflow: hidden;          /* the mask each glyph rises out of */
  vertical-align: bottom;
}
.kt__char > span {
  display: inline-block;
  transform: translateY(102%);
  opacity: 0;
  transition: transform 640ms var(--ease-out), opacity 420ms var(--ease-out);
}
.kt.is-in .kt__char > span { transform: none; opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .band__track { animation: none; transform: none !important; }
  .band__run { transition: none; }
  .kt__char > span { transform: none; opacity: 1; transition: none; }
}

/* ================================================================== home == */
/* The front page keeps the shape aviationhead.com already has: open on a
   photograph, say what the work is, then walk every aircraft in turn with its
   own frame and its own paragraph. The sequence is his. What this rebuild
   changes is the craft around it, not the order or the format.
   The turntables, the roster and the plates all still exist; they are a
   section reached from here rather than the first thing a visitor meets. */

.home { background: var(--paper); }

/* The corner greeting names the photographer in the top right of every page.
   On the home page the opening frame runs nearly full width beneath it, so it
   landed as pale grey text on top of a photograph, and it repeated a name the
   h1 states two lines below at 8rem. The contact pill stays, because that is
   the one thing the client asked to persist everywhere. */
.home .rail__intro { display: none; }

/* ---- the opening frame ---------------------------------------------------
   minmax(0, 1fr) rather than 1fr, deliberately. A content-sized grid row grows
   to the intrinsic height of the photograph inside it, which makes height:100%
   on that photograph resolve circularly and overflow:hidden crop it. That bug
   has already cost this project twice, on the reel stage and in the lightbox.
   minmax(0, ...) is the fix: the row is allowed to be shorter than its content. */
.opener {
  min-height: 100svh;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: clamp(var(--space-xl), 4vw, var(--space-3xl));
  padding: calc(var(--space-5xl) * 0.62) 0 var(--space-3xl);
}
.opener__media {
  position: relative;
  min-height: 0;
  overflow: hidden;
  margin: 0 var(--page-x);
}
/* `cover` cropped the frame to fill the row, and what it cropped off the right
   edge was the photographer's own copyright mark. Nothing on a photography site
   may crop the photographs. Height follows the aspect, capped, and `contain`
   handles the cap: the bars it leaves are white on white. */
.opener__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
}
.opener__type { padding: 0 var(--page-x); }
.opener__sub {
  margin-top: var(--space-md);
  font-size: var(--text-lg);
  color: var(--ink-70);
  max-width: var(--measure);
}
.opener__scroll {
  padding: 0 var(--page-x);
  display: flex; align-items: center; gap: var(--space-xs);
}
.opener__scroll::after {
  content: ""; width: 34px; height: 1px; background: var(--ink-30);
}

/* ---- what the work is ---------------------------------------------------- */
.statement {
  padding: var(--space-5xl) var(--page-x);
  border-top: var(--rule) solid var(--hairline);
}
.statement__type { max-width: 78ch; }
.statement .prose { margin-top: var(--space-lg); max-width: var(--measure); }

/* ---- every aircraft, in turn --------------------------------------------- */
.run { border-top: var(--rule) solid var(--hairline); }

.entry {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(var(--space-xl), 5vw, var(--space-4xl));
  align-items: center;
  padding: var(--space-5xl) var(--page-x);
  border-bottom: var(--rule) solid var(--hairline);
  /* 21 entries, each with a full-size photograph. Skipping layout and paint
     for the ones nowhere near the viewport is the difference between a page
     that scrolls at frame rate and one that does not. The intrinsic size stops
     the scrollbar jumping as they resolve. */
  content-visibility: auto;
  contain-intrinsic-size: auto 720px;
}
/* Alternate the side the photograph sits on. Explicit placement rather than
   `order`, so the DOM sequence a screen reader follows never changes. */
.entry:nth-child(even) .entry__media { grid-column: 2; grid-row: 1; }
.entry:nth-child(even) .entry__type  { grid-column: 1; grid-row: 1; }

.entry__media { margin: 0; overflow: hidden; }
.entry__media img {
  display: block;
  width: 100%; height: auto;
  transition: scale var(--dur-lg) var(--ease-out);
}
.entry:hover .entry__media img { scale: 1.014; }

.entry__name { margin-top: var(--space-sm); }
.entry__name a { color: inherit; }
.entry__role { margin-top: var(--space-sm); }
.entry .prose { margin-top: var(--space-lg); max-width: var(--measure); }

.entry__links {
  margin-top: var(--space-xl);
  display: flex; flex-wrap: wrap;
  gap: var(--space-lg);
}
.entry__link {
  font-family: var(--font-mono);
  font-size: var(--text-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  padding-bottom: 3px;
  border-bottom: var(--rule) solid var(--hairline-mid);
  transition: border-color var(--dur-sm) var(--ease-out);
}
.entry__link:hover { border-bottom-color: currentColor; }

/* ---- his own line about the work ----------------------------------------- */
.quote {
  padding: var(--space-5xl) var(--page-x);
  text-align: center;
  border-bottom: var(--rule) solid var(--hairline);
}
.quote blockquote {
  margin: 0 auto;
  max-width: 26ch;
  text-wrap: balance;
}
.quote .label { margin-top: var(--space-xl); }

/* ---- where the work continues -------------------------------------------- */
.connect { padding: var(--space-5xl) var(--page-x); }
.connect .prose { margin-top: var(--space-lg); max-width: var(--measure); }

@media (max-width: 860px) {
  .entry {
    grid-template-columns: 1fr;
    gap: var(--space-xl);
    padding: var(--space-4xl) var(--page-x);
  }
  /* One column: the placement above would strand the photograph in a second
     column that no longer exists. */
  .entry:nth-child(even) .entry__media,
  .entry:nth-child(even) .entry__type { grid-column: 1; grid-row: auto; }
  .opener { min-height: 92svh; }
}

@media (prefers-reduced-motion: reduce) {
  .entry:hover .entry__media img { scale: 1; }
}
