/* ============================================================================
   Thermal Sky - site styles
   tokens.css is the locked source of truth. Every color, size, radius, and
   duration below is a tokens.css custom property. No raw hex lives in this
   file. The only new custom properties declared here are non-color scale
   values (the two cell figure clamps and the slider position).

   Discipline rules for the whole page, stated once:
   1. AMBER marks the leading price figure in its block, plus the focus
      outline tokens.css defines. The two legal pages carry no amber at all.
      Three figures wear it: the hero cell, and the two standard rates, which
      are what a client pays from the fourth project onward. Founding rates
      sit beside the standard rates in Mist, never amber, so a pair never
      reads as a sale price against an inflated anchor. No amber in the
      manifest, the slider, or anywhere else.
   2. EMBER is the primary button fill and the link hover color only.
      ROSE is form failure only: the inline field errors and the submit
      failure alert. VIOLET is unused in v1 (2.8:1 on Void).
   3. Every data numeral outside running prose is IBM Plex Mono. Archivo never
      sets a digit. There is no billboard price anywhere; a figure at
      instrument scale reads as fact.
   4. Square corners are contained to the cell clusters and the slider frame
      corner ticks. Buttons, inputs, panels, and cards keep the locked radius
      scale.
   5. Mist floor: no Mist text below 14px except eyebrow-class labels (mono,
      uppercase, 2px tracking, 10 to 13px), which BRAND.md sanctions. Mist is
      for genuine metadata only: eyebrows, captions, hints, notes. Prose a
      buyer has to read is Paper, whatever its size.
   6. Sections sit on Void and are separated by a 1px hairline, generous
      padding, and ruled blocks. The request section is the one Graphite
      band. On a Graphite band, anything that would itself be Graphite
      (tables, placeholders, inputs) drops to Void instead: the inset-panel
      step BRAND.md describes, never two greys touching.
   ============================================================================ */

/* --- Fonts: self-hosted latin subsets, all SIL Open Font License 1.1 ------ */

/* The page asks Archivo for exactly one instance (900 at 118%) across three
   heading styles, so what ships is that static instance rather than the
   whole 100-900 x 62-125 design space: 90KB down to 14KB on the one
   render-critical preloaded face. The two descriptors below are what the
   instance is, so .ts-display's 900 / 118% matches it exactly. */
@font-face {
  font-family: 'Archivo';
  src: url('assets/fonts/archivo-900-118-latin.woff2') format('woff2');
  font-weight: 900;
  font-stretch: 118%;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Space Grotesk';
  src: url('assets/fonts/space-grotesk-var-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'IBM Plex Mono';
  src: url('assets/fonts/plex-mono-400-latin.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'IBM Plex Mono';
  src: url('assets/fonts/plex-mono-500-latin.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* Fallback face with adjusted metrics: on a slow connection the headline
   paints in this before Archivo arrives, at close to Archivo's width, so the
   swap moves the layout as little as possible. */
@font-face {
  font-family: 'Archivo Fallback';
  src: local('Arial Bold'), local('Arial'), local('Helvetica Bold'), local('Helvetica');
  font-weight: 900;
  size-adjust: 112%;
  ascent-override: 92%;
  descent-override: 22%;
  line-gap-override: 0%;
}
.ts-display { font-family: 'Archivo', 'Archivo Fallback', sans-serif; }

/* --- Non-color scale values not covered by tokens.css --------------------- */

:root {
  --ts-cell-value: clamp(16px, 1.7vw, 20px);
  --ts-cell-figure: clamp(22px, 2.4vw, 28px);
  /* Sticky nav height. The anchor scroll-margin below is derived from this,
     so the bar and the landing offset can never drift apart. */
  --ts-nav-h: 64px;
}

/* --- Page frame ----------------------------------------------------------- */

/* Dark native UI: scrollbars, form controls, and the date picker all follow
   this rather than rendering light against a near-black page. */
:root { color-scheme: dark; }

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .hero-cells .cell { animation-delay: 0s; }
}

img, svg, video { max-width: 100%; }
/* Without this the width/height attributes every swap template carries
   (kept for CLS) would hold the height while max-width shrank the width. */
img, video { height: auto; }

/* No double-tap zoom delay, and no default blue flash on tap: the elements
   below carry their own hover, active, and focus feedback. */
a, button, summary, input, textarea, label {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* Anchor targets clear the sticky bar, then land with air above them rather
   than flush under it. */
main, section[id], .block-caption[id],
.legal h2[id], .legal h3[id], .legal h4[id] {
  scroll-margin-top: calc(var(--ts-nav-h) + var(--ts-space-5));
}

/* Long unbroken strings (a pasted filename, a long address) wrap instead of
   widening their column. */
.datatable-a, .datatable-b, .cell-value, .spec-value { overflow-wrap: break-word; }

/* Visible only when tabbed to. */
.skip-link {
  position: absolute;
  top: var(--ts-space-2);
  left: -9999px;
  z-index: 10;
  background: var(--ts-graphite);
  color: var(--ts-paper-text);
  border: 1px solid var(--ts-hairline);
  border-radius: var(--ts-radius-input);
  padding: 12px 18px;
  font: 500 15px var(--ts-font-body);
  text-decoration: none;
}
.skip-link:focus { left: var(--ts-space-5); }

.wrap {
  width: 100%;
  max-width: var(--ts-content-max);
  margin: 0 auto;
  padding-left: max(var(--ts-space-5), env(safe-area-inset-left));
  padding-right: max(var(--ts-space-5), env(safe-area-inset-right));
}

.wrap--narrow { max-width: 560px; }

/* BRAND.md calls for 96 to 120px of vertical section padding on desktop. The
   generous step is what keeps neighbouring sections from reading as one
   continuous wall of text. */
.section { padding: var(--ts-space-9) 0; }
.section + .section { border-top: 1px solid var(--ts-hairline); }

.section-h2 {
  font-size: var(--ts-text-h2);
  text-wrap: balance;
  margin: var(--ts-space-4) 0 var(--ts-space-7);
  max-width: 22ch;
}
/* Most sections open on their heading, with no eyebrow above it to clear. */
.section-h2:first-child { margin-top: 0; }

/* Alternating grounds. A Graphite band and a Void band cannot blur into
   each other the way two Void sections do. */
.section--panel { background: var(--ts-graphite); }
.section--panel .field-input { background: var(--ts-void); }

.lead {
  font-size: var(--ts-text-body);
  color: var(--ts-paper-text);
  max-width: 62ch;
  margin: 0 0 var(--ts-space-6);
}

/* Eyebrows carry no margin of their own. Both rules are single-class, so the
   caption below simply wins by order. */
.ts-eyebrow { margin: 0; }

/* Small mono caption naming the block beneath it, so no instrument row ever
   appears without saying what it is. */
.cluster-caption { margin: var(--ts-space-8) 0 var(--ts-space-4); }

/* --- Nav ------------------------------------------------------------------ */

/* Sticky: the bar carries the only persistent route to the form on a page
   this long. Opaque Void, because the header has no surface of its own and
   content would otherwise scroll visibly under the logo. z-index sits under
   the skip link (10) so a tabbed-to skip link still clears it. */
.nav {
  border-bottom: 1px solid var(--ts-hairline);
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--ts-void);
}

/* Carries .wrap for its width, centring, and safe-area padding. */
.nav-inner {
  min-height: var(--ts-nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ts-space-4);
}

/* Padding, not height: the bar is already 64px, so the larger hit area
   costs no layout (m23). */
.nav-logo-link { display: inline-flex; align-items: center; min-width: 0; padding: 11px 0; }
/* max-height rather than height, so a squeezed flex line scales the logo
   proportionally instead of letterboxing it inside a fixed 28px box. */
.nav-logo { display: block; height: auto; max-height: 28px; width: auto; max-width: 100%; }

/* The bar's CTA is the secondary button recipe (45px tall, so it clears the
   44px tap floor); this only stops it wrapping or shrinking in the flex row. */
.nav-cta {
  white-space: nowrap;
  flex: none;
}

/* Line of mono under the hero frame naming the palette and its range, so the
   image reads as data and not as a picture. */
.hero-legend { margin-top: var(--ts-space-3); font-size: 10px; letter-spacing: 1.5px; }

/* --- Signature: telemetry cell clusters ----------------------------------- */
/* Square corners are deliberate here and nowhere else. Contiguous cells share
   interior hairlines with zero gap so a cluster reads as one instrument.
   Cells are inert: no hover, no cursor change. */

.cell-cluster {
  position: relative;
  display: grid;
  background: var(--ts-void);
  border: 1px solid var(--ts-hairline);
}

.cell-cluster::before,
.cell-cluster::after {
  content: "";
  position: absolute;
  width: 10px;
  height: 10px;
  border: 0 solid var(--ts-mist);
  pointer-events: none;
}
.cell-cluster::before {
  top: -1px; left: -1px;
  border-top-width: 1px;
  border-left-width: 1px;
}
.cell-cluster::after {
  bottom: -1px; right: -1px;
  border-bottom-width: 1px;
  border-right-width: 1px;
}

.cell {
  display: flex;
  flex-direction: column;
  gap: var(--ts-space-2);
  padding: var(--ts-space-4) var(--ts-space-5);
  min-width: 0;
}
.cell + .cell { border-left: 1px solid var(--ts-hairline); }

.cell-label { display: block; }

.cell-value {
  font: 500 var(--ts-cell-value) var(--ts-font-mono);
  line-height: 1.3;
  color: var(--ts-paper-text);
}
.cell-value--fig { font-size: var(--ts-cell-figure); }
.cell-value--amber { color: var(--ts-amber); }
/* A figure that is real but secondary to the one beside it. */
.cell-value--quiet { color: var(--ts-mist); }

/* Values that are a line of words rather than a figure. */
.cell-value--line { font: 400 15px/1.45 var(--ts-font-mono); }

/* --- Media placeholders --------------------------------------------------- */
/* Every box carries aspect-ratio so the later img swap causes zero shift. */

.placeholder {
  background: var(--ts-graphite);
  border: 1px solid var(--ts-hairline);
  border-radius: var(--ts-radius-panel);
  display: grid;
  place-items: center;
  padding: var(--ts-space-5);
}

.ph-label {
  font: 400 var(--ts-text-small)/1.55 var(--ts-font-mono);
  color: var(--ts-mist);
  text-align: center;
  max-width: 34ch;
}

/* .placeholder--hero takes its height from the hero row while the two columns
   sit side by side, so the text and media columns stay flush. Once the grid
   stacks at 720px it picks up the sensor's own 640/512 ratio instead. */
.placeholder--headshot { aspect-ratio: 3 / 4; }
.placeholder--video { aspect-ratio: 16 / 9; }
.placeholder--fill { border: 0; border-radius: 0; }
.placeholder--package { min-height: 148px; }

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

/* The media column stretches to the exact height of the text column, so both
   share a top and a bottom edge. Centering it instead left uneven air above
   and below, which is what read as loose spacing. */
.hero-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--ts-space-7);
  align-items: stretch;
  margin-bottom: var(--ts-space-7);
}

.hero-text { grid-column: 1 / 8; }

/* The location was a stray 11px line. As a label and a value split by a
   hairline it reads as an instrument caption and carries its own weight. */
.hero-eyebrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ts-space-3);
  font-size: 13px;
  letter-spacing: 2.5px;
}
.hero-eyebrow-value {
  color: var(--ts-paper-text);
  padding-left: var(--ts-space-3);
  border-left: 1px solid var(--ts-hairline);
}

.hero-media {
  grid-column: 8 / 13;
  position: relative;
  display: flex;
  flex-direction: column;
  margin: 0;
  min-height: 340px;
}
/* The frame stretches to the text column's full height so both share a top
   and a bottom edge. */
.hero-media > img {
  flex: 1;
  min-height: 0;
  width: 100%;
  object-fit: cover;
  border-radius: var(--ts-radius-panel);
}

.hero-h1 {
  font-size: var(--ts-text-h1);
  margin: var(--ts-space-4) 0 var(--ts-space-5);
}
.hero-h1-b {
  display: block;
  font-size: clamp(19px, 2.2vw, 24px);
  color: var(--ts-mist);
  margin-top: var(--ts-space-2);
}

/* One lede paragraph. Seasonality and conditions live in the pricing
   datatable, not above the fold. */
.hero-sub {
  font-size: var(--ts-text-body);
  color: var(--ts-paper-text);
  max-width: 52ch;
  margin: 0 0 var(--ts-space-4);
}
.hero-sub:last-of-type { margin-bottom: var(--ts-space-6); }

.hero-cta { display: flex; flex-wrap: wrap; gap: var(--ts-space-3); }

/* Anchors carrying the token button recipes. */
a.ts-btn,
a.ts-btn-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 45px;
  text-decoration: none;
}
.ts-btn:active { background: var(--ts-btn-hover); }

a.ts-btn { color: var(--ts-ink); }
a.ts-btn:hover { color: var(--ts-ink); }
a.ts-btn-secondary { color: var(--ts-paper-text); }
a.ts-btn-secondary:hover { color: var(--ts-paper-text); }

/* Four tiles of different widths. Auto tracks take their content first and
   share what is left; every value is short enough to hold one line at the
   content max, so the four cells stay one even row. */
.hero-cells { grid-template-columns: repeat(4, auto); }
.hero-cells .cell-value { white-space: nowrap; }
/* A hero cell never shrinks below its own content: if a value ever grows past
   the row, the strip overflows visibly instead of one cell bleeding into the
   next. */
.hero-cells .cell { min-width: max-content; }

/* One orchestrated beat on first paint, then near-silence. CSS only, so no
   JS is involved and no layout is affected (opacity and transform only). */
@keyframes ts-cell-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
.hero-cells .cell {
  animation: ts-cell-in var(--ts-dur) var(--ts-ease) both;
}
.cell--load-1 { animation-delay: 0ms; }
.cell--load-2 { animation-delay: 60ms; }
.cell--load-3 { animation-delay: 120ms; }
.cell--load-4 { animation-delay: 180ms; }

/* --- Data table: capture pack manifest and scope ledger --------------------
   One component used twice, so both tables on the page are the same object.
   Column headings sit on the same grid as the rows beneath them, so a heading
   always lines up with the column it heads, and the centre rule runs the full
   height of every row. */

.datatable {
  background: var(--ts-graphite);
  border: 1px solid var(--ts-hairline);
  border-radius: var(--ts-radius-panel);
  padding: var(--ts-space-4) var(--ts-space-6) var(--ts-space-5);
}
.datatable-head,
.datatable-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: stretch;
}

.datatable-head { border-bottom: 1px solid var(--ts-hairline); }
.datatable-head span {
  font: 500 var(--ts-text-eyebrow) var(--ts-font-mono);
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--ts-label);
  padding-top: 0;
  padding-bottom: var(--ts-space-3);
}

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

.datatable-row { border-bottom: 1px solid var(--ts-hairline); }

.datatable-a { padding: var(--ts-space-4) var(--ts-space-5) var(--ts-space-4) 0; }
.datatable-b {
  padding: var(--ts-space-4) 0 var(--ts-space-4) var(--ts-space-5);
  border-left: 1px solid var(--ts-hairline);
}

.datatable-a,
.datatable-b {
  font-size: 16px;
  line-height: 1.5;
  color: var(--ts-paper-text);
}
/* The manifest lists files, so its left column stays mono and its right
   column explains in plain prose. */
.datatable-a--mono { font: 500 15px/1.5 var(--ts-font-mono); }
/* Paper, not Mist: the manifest detail is what a buyer reads to decide. */
.datatable-b--quiet {
  font-size: 15px;
  color: var(--ts-paper-text);
}
/* One line of terms under the manifest, separated from the deliverables. */
.datatable-note {
  margin: var(--ts-space-4) 0 0;
  font: 400 var(--ts-text-small)/1.6 var(--ts-font-mono);
  color: var(--ts-mist);
}

/* --- 2. Capture pack: RGB to thermal slider -------------------------------- */

/* Names the block underneath. Distinct from .cluster-caption, which opens a
   new subject and therefore carries far more air above it. */
.block-caption { margin: var(--ts-space-8) 0 var(--ts-space-3); }
.block-caption + .compare,
.block-caption + .placeholder { margin-top: 0; }

.compare { margin: var(--ts-space-7) 0 var(--ts-space-5); max-width: 880px; }

.compare-frame {
  --pos: 50%;
  position: relative;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--ts-hairline);
  border-radius: var(--ts-radius-panel);
  overflow: hidden;
  /* The frame is a fixed box with hidden overflow, so the layout the slider
     forces on every input event has nowhere to escape to. */
  contain: layout paint;
}
/* :focus-within first, so a browser without :has() still shows a ring. It
   fires on pointer focus too; the :has pair below narrows that back to
   keyboard focus wherever :has is supported. */
.compare-frame:focus-within {
  outline: 2px solid var(--ts-amber);
  outline-offset: 2px;
}
.compare-frame:has(.compare-range:focus) { outline: 0; }
.compare-frame:has(.compare-range:focus-visible) {
  outline: 2px solid var(--ts-amber);
  outline-offset: 2px;
}

/* Spot-meter corner ticks; square by design, like the cell clusters. */
.compare-frame::before,
.compare-frame::after {
  content: "";
  position: absolute;
  z-index: 3;
  width: 10px;
  height: 10px;
  border: 0 solid var(--ts-mist);
  pointer-events: none;
  transition: border-color var(--ts-dur) var(--ts-ease);
}
.compare-frame::before {
  top: 0; left: 0;
  border-top-width: 1px;
  border-left-width: 1px;
}
.compare-frame::after {
  bottom: 0; right: 0;
  border-bottom-width: 1px;
  border-right-width: 1px;
}
.compare-frame:has(.compare-range:active)::before,
.compare-frame:has(.compare-range:active)::after,
.compare-frame.is-dragging::before,
.compare-frame.is-dragging::after { border-color: var(--ts-paper-text); }

/* Once script.js takes over pointer input the frame itself is the drag
   surface: any press or horizontal drag moves the split, while pan-y leaves
   vertical page scrolling to the browser. The range input drops out of hit
   testing but stays in the tab order for arrow keys and screen readers. */
.compare-frame.has-pointer {
  cursor: ew-resize;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
}
.compare-frame.has-pointer .compare-range { pointer-events: none; }
.compare-pane > img {
  pointer-events: none;
  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
}

.compare-pane { position: absolute; inset: 0; }
/* One line covers the placeholder div and the real img identically, so the
   asset swap stays a one-line change per pane. */
.compare-pane > * { position: absolute; inset: 0; object-fit: cover; }
/* A replaced element keeps its intrinsic size under inset: 0, so the real
   img needs explicit 100% box dimensions for object-fit to take effect. */
.compare-pane > img { width: 100%; height: 100%; }
.compare-pane--thermal { clip-path: inset(0 0 0 var(--pos)); }
/* Each label centres inside the visible part of its own pane, so it follows
   the divider instead of sitting under the other pane. */
.compare-pane--rgb .placeholder { padding-right: calc(100% - var(--pos)); }
.compare-pane--thermal .placeholder { padding-left: var(--pos); }

.compare-divider {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--pos);
  width: 1px;
  background: var(--ts-paper-text);
  z-index: 2;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.compare-grip {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  background: var(--ts-graphite);
  border: 1px solid var(--ts-hairline);
  border-radius: var(--ts-radius-input);
  font: 500 14px var(--ts-font-mono);
  letter-spacing: 1px;
  color: var(--ts-paper-text);
}

.compare-range {
  position: absolute;
  inset: 0;
  z-index: 4;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  opacity: 0;
  background: transparent;
  border: 0;
  cursor: ew-resize;
  touch-action: pan-y;
  -webkit-appearance: none;
  appearance: none;
}
.compare-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 44px;
  height: 100%;
  border: 0;
  cursor: ew-resize;
}
.compare-range::-moz-range-thumb {
  width: 44px;
  height: 100%;
  border: 0;
  border-radius: 0;
  cursor: ew-resize;
}

.compare-foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ts-space-3);
  justify-content: space-between;
  align-items: baseline;
  padding-top: var(--ts-space-3);
}
/* Both wear .ts-eyebrow for the recipe; the readout only differs in colour,
   and .compare-hint exists purely as the no-JS hook below. */
.compare-readout { color: var(--ts-paper-text); }
/* No keyboard to speak of on a touch-only device, so the hint keeps just the
   half that applies. */
@media (hover: none) and (pointer: coarse) {
  .compare-hint-keys { display: none; }
}

/* What the colours in the thermal pane mean, under every pair. */
.compare-legend {
  margin: var(--ts-space-3) 0 0;
  font: 400 var(--ts-text-small)/1.6 var(--ts-font-mono);
  color: var(--ts-mist);
  max-width: 72ch;
}

/* Without JS the split stays an informative static 50/50 and nothing
   pretends to be interactive. */
html:not(.js) .compare-range,
html:not(.js) .compare-grip,
html:not(.js) .compare-hint { display: none; }

/* The two sample downloads side by side; full width on a phone. */
.sample-actions { display: flex; flex-wrap: wrap; gap: var(--ts-space-3); }

/* Mono, per the brief. */
.sample-note {
  margin: var(--ts-space-4) 0 0;
  font: 400 var(--ts-text-small)/1.6 var(--ts-font-mono);
  color: var(--ts-mist);
  max-width: 62ch;
}

.proof-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--ts-space-6);
  margin-top: var(--ts-space-8);
}
.proof { margin: 0; }
.proof-img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--ts-hairline);
  border-radius: var(--ts-radius-panel);
}
.proof-caption {
  margin: var(--ts-space-4) 0 0;
  font-size: var(--ts-text-small);
  color: var(--ts-mist);
}

/* --- 3. Scope -------------------------------------------------------------- */

/* Two lists, not a table: the rows opposite each other are not pairs, and a
   second bordered table one section below the first read as a repeat. */
.split-points {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--ts-space-7);
  margin-top: var(--ts-space-7);
}
.points-col + .points-col {
  padding-left: var(--ts-space-7);
  border-left: 1px solid var(--ts-hairline);
}
.points-head {
  margin: 0 0 var(--ts-space-4);
  padding-bottom: var(--ts-space-3);
  border-bottom: 1px solid var(--ts-hairline);
}
.points-list { list-style: none; margin: 0; padding: 0; }
.point {
  position: relative;
  padding: var(--ts-space-3) 0 var(--ts-space-3) var(--ts-space-5);
  font-size: var(--ts-text-body);
  color: var(--ts-paper-text);
}
/* A square marker, the same 6px square the cell clusters use for their
   corner ticks. Nothing round on this site. */
.point::before {
  content: "";
  position: absolute;
  left: 0;
  top: calc(var(--ts-space-3) + .5em);
  width: 7px;
  height: 7px;
  border: 1px solid var(--ts-mist);
}
/* Solid for the work Thermal Sky performs, open for the work that stays with
   the client's team, so the two columns read apart at a glance. */
.points-col:first-child .point::before {
  background: var(--ts-paper-text);
  border-color: var(--ts-paper-text);
}

.spec-rows { margin: 0; }
.spec-row {
  display: grid;
  grid-template-columns: 215px 1fr;
  gap: var(--ts-space-5);
  align-items: baseline;
  padding: var(--ts-space-4) 0;
  border-top: 1px solid var(--ts-hairline);
}
.spec-row dt { margin: 0; }
.spec-value {
  margin: 0;
  font-size: var(--ts-text-body);
  color: var(--ts-paper-text);
}
/* Values that are instrument readings rather than sentences. */
.spec-value--mono {
  font: 500 16px var(--ts-font-mono);
}

/* --- 4. Procedure ---------------------------------------------------------- */

/* Each step carries its own rule, so the four read as four discrete cells in
   a sequence instead of one banded paragraph. */
.steps {
  list-style: none;
  margin: 0 0 var(--ts-space-8);
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--ts-space-6);
}
.step {
  border-top: 1px solid var(--ts-hairline);
  padding-top: var(--ts-space-4);
}

/* Marks, not illustration: one hairline weight, no fill, currentColor, and
   only where the mark carries information the words do not. Decorative, so
   every one is aria-hidden. */
.ic {
  width: 22px;
  height: 22px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.25;
  stroke-linecap: square;
  stroke-linejoin: miter;
  color: var(--ts-mist);
}
.ic--sm { width: 16px; height: 16px; stroke-width: 1.4; }
/* A solid shape inside an otherwise stroked icon. */
.ic .ic-fill { fill: currentColor; stroke: none; }

.step-head {
  display: flex;
  align-items: center;
  gap: var(--ts-space-3);
}

.step-num {
  font: 500 12px var(--ts-font-mono);
  letter-spacing: 2px;
  color: var(--ts-label);
}

.spec-dt {
  display: flex;
  align-items: center;
  gap: var(--ts-space-3);
}
.step-name {
  font: 500 18px var(--ts-font-body);
  color: var(--ts-paper-text);
  margin: var(--ts-space-3) 0 var(--ts-space-2);
}
.step-detail {
  margin: 0;
  font-size: 15px;
  color: var(--ts-paper-text);
}

/* --- 5. Pricing ------------------------------------------------------------ */
/* No strikethrough, no badges, no highlighted tier. The standard rate is the
   real price, so it takes the amber; the founding rate steps back to Mist
   beside it. The reverse, a bright founding figure against a plain standard
   one, reads as a sale price against an inflated anchor, which is exactly
   the pattern the brief bans. */

.tiers { margin-bottom: var(--ts-space-7); }

.tier {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--ts-space-6);
  align-items: center;
  padding: var(--ts-space-6) 0;
  border-top: 1px solid var(--ts-hairline);
}
.tier:last-child { border-bottom: 1px solid var(--ts-hairline); }

.tier-name {
  font: 500 20px var(--ts-font-body);
  color: var(--ts-paper-text);
  margin: 0 0 var(--ts-space-2);
}
/* States that the lines beneath are alternatives, not contents. The manifest
   above uses the same square marker to mean "all of these", so without this
   clause three lines under one price read as three jobs for the money. Mist
   because it is a qualifier on the list, not copy in its own right. */
.tier-lead {
  margin: 0 0 var(--ts-space-2);
  font-size: 15px;
  color: var(--ts-mist);
}

/* What the tier covers, one line per job type, so a reader can find the
   four-storey rule or the square footage without parsing a sentence. Same
   square marker as the scope points, tighter rhythm because it sits inside
   a card. */
.tier-scope {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 15px;
  line-height: 1.45;
  color: var(--ts-paper-text);
  max-width: 72ch;
}
.tier-scope li {
  position: relative;
  padding-left: var(--ts-space-4);
}
.tier-scope li + li { margin-top: var(--ts-space-2); }
.tier-scope li::before {
  content: "";
  position: absolute;
  left: 0;
  top: .55em;
  width: 6px;
  height: 6px;
  border: 1px solid var(--ts-mist);
}

.tier-cells { grid-template-columns: auto auto; }

/* The terms under the tiers are the same two-column ledger as the
   capture-pack manifest. Column B stays Paper, not Mist: these are terms the
   reader has to get through, not small print. The gap above it is the
   .tiers margin-bottom. */

/* The ledger above already closes with its own border, so the strip carries
   no rule of its own: two hairlines 24px apart read as a mistake. */
.terms-strip {
  margin: var(--ts-space-5) 0 0;
  font: 400 var(--ts-text-small) var(--ts-font-mono);
  color: var(--ts-mist);
}

/* --- 6. Operator ----------------------------------------------------------- */
/* The section heading lives in the right column beside the headshot rather
   than above it, so the portrait and the words about the person occupy the
   same block instead of stacking into a tall, half-empty column. */

.operator-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--ts-space-7);
  /* Stretch, so the portrait and the credentials block end on one line. The
     row takes the taller column; the portrait fills it and crops its sides
     (plain wall) rather than leaving a gap under the photo. */
  align-items: stretch;
}
.operator-media { grid-column: 1 / 6; min-width: 0; }
.operator-photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 30%;
  border: 1px solid var(--ts-hairline);
  border-radius: var(--ts-radius-panel);
}
.operator-bio { grid-column: 6 / 13; }

.operator-h2 { margin: var(--ts-space-3) 0 var(--ts-space-5); }

.operator-text {
  margin: 0 0 var(--ts-space-4);
  font-size: var(--ts-text-body);
  color: var(--ts-paper-text);
  max-width: 62ch;
}

.operator-contact {
  margin: 0;
  font-size: var(--ts-text-body);
  color: var(--ts-paper-text);
}
/* Links inside prose are underlined: colour alone is not a distinguishable
   difference (WCAG 1.4.1). */
.operator-contact a,
.form-alt a,
.form-alert a,
.footer-legal a,
.legal a { text-decoration: underline; text-underline-offset: 2px; }
/* The phone number is the link a contractor actually taps: give it and the
   email a 44px row without changing the line they sit on. */
.operator-contact a,
.form-alt a { display: inline-block; padding: 9px 0; margin: -9px 0; }

/* Three rows, one block: the two certificates plus the insurance line. Each
   keeps its own label, so nothing calls the insurance a credential held.
   Stacked rather than side by side, because these are sentences, not
   figures, and one block ends flush with the portrait instead of trailing
   off below it. */
.certificates { grid-template-columns: 1fr; }
.certificates .cell + .cell {
  border-left: 0;
  border-top: 1px solid var(--ts-hairline);
}

/* Full stop between the headshot block and the video. */
.operator-video { margin-top: var(--ts-space-8); }
.operator-video-player {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--ts-graphite);
  border: 1px solid var(--ts-hairline);
  border-radius: var(--ts-radius-panel);
}

/* --- 6b. Aircraft ---------------------------------------------------------- */

.aircraft-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--ts-space-7);
  align-items: start;
}
.aircraft-media { grid-column: 1 / 7; }
.aircraft-specs { grid-column: 7 / 13; }
.aircraft-photo {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border: 1px solid var(--ts-hairline);
  border-radius: var(--ts-radius-panel);
}


.aircraft-text {
  margin: 0 0 var(--ts-space-6);
  font-size: var(--ts-text-body);
  color: var(--ts-paper-text);
  max-width: 62ch;
}

/* --- 7. FAQ ---------------------------------------------------------------- */
/* Native details/summary. Open and close is instant; mechanical is on brand
   and content height is never animated. */

.faq-item { border-top: 1px solid var(--ts-hairline); }
.faq-item:last-child { border-bottom: 1px solid var(--ts-hairline); }

.faq-q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ts-space-4);
  min-height: 48px;
  padding: var(--ts-space-3) 0;
  font: 500 17px var(--ts-font-body);
  color: var(--ts-paper-text);
  cursor: pointer;
  list-style: none;
}
.faq-q::-webkit-details-marker { display: none; }

.faq-q::after {
  /* Alternative-text syntax: drawn, never announced. */
  content: "+" / "";
  flex: none;
  font: 400 18px var(--ts-font-mono);
  color: var(--ts-mist);
  transition: transform var(--ts-dur) var(--ts-ease), color var(--ts-dur) var(--ts-ease);
}
.faq-item[open] .faq-q::after { transform: rotate(45deg); color: var(--ts-paper-text); }
.faq-item:hover .faq-q::after { color: var(--ts-paper-text); }

/* tokens.css does not cover summary, so the focus ring is declared here. */
.faq-q:focus-visible {
  outline: 2px solid var(--ts-amber);
  outline-offset: 2px;
}

.faq-a {
  padding: 0 0 var(--ts-space-5);
  max-width: 62ch;
}
/* Deliberately not Paper: the summary above it is Paper, and an answer that
   matches its own question gives the eye nothing to separate them. Mist is
   7.03:1 here, comfortably readable. */
.faq-a p {
  margin: 0;
  font-size: 16px;
  color: var(--ts-mist);
}

/* --- 8. Request form ------------------------------------------------------- */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--ts-space-2);
  margin-bottom: var(--ts-space-5);
}

/* The job-specific fields are hidden with the hidden attribute, and a flex
   container outranks the UA sheet's display: none. Stated once here rather
   than as !important on the reset, because .field is the only element on the
   page that both sets display and gets hidden. */
.field[hidden] { display: none; }

.field-label { display: block; }

.field-input {
  width: 100%;
  background: var(--ts-graphite);
  /* Mist, not hairline: an input boundary is a UI component boundary and
     owes 3:1. Label grey is 5.9:1 on Graphite, hairline is 1.3:1. */
  border: 1px solid var(--ts-label);
  border-radius: var(--ts-radius-input);
  color: var(--ts-paper-text);
  font: 400 15px var(--ts-font-body);
  padding: 12px 14px;
  transition: border-color var(--ts-dur) var(--ts-ease);
}
.field-input:hover { border-color: var(--ts-paper-text); }
.field-textarea { resize: vertical; min-height: 108px; }

/* color-scheme makes the browser's own calendar render dark, so it matches
   the page instead of flashing a white panel. The popup itself is browser
   chrome and cannot be styled further; what can be styled is the field and
   its icon, and script.js opens the calendar from a click anywhere in the
   field rather than only on the icon. */
.field-input[type="date"] {
  min-height: 45px;
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.field-input[type="date"]::-webkit-calendar-picker-indicator {
  cursor: pointer;
  opacity: .6;
  transition: opacity var(--ts-dur) var(--ts-ease);
}
.field-input[type="date"]:hover::-webkit-calendar-picker-indicator,
.field-input[type="date"]:focus::-webkit-calendar-picker-indicator { opacity: 1; }

.field-hint {
  margin: 0;
  font: 400 var(--ts-text-small)/1.5 var(--ts-font-body);
  color: var(--ts-mist);
}

.field-input[aria-invalid="true"] { border-color: var(--ts-rose); }

.field-error {
  margin: 0;
  font: 400 var(--ts-text-small)/1.5 var(--ts-font-body);
  color: var(--ts-rose);
}

.form-submit { margin-top: var(--ts-space-2); min-height: 45px; }
.form-submit:disabled { cursor: default; }

.form-alt {
  margin: var(--ts-space-4) 0 0;
  font-size: var(--ts-text-small);
  color: var(--ts-mist);
}

/* Sits under the "or email" line, so it needs its own tighter step and a
   smaller size: it is a notice, not an alternative route. */
.form-privacy {
  margin-top: var(--ts-space-3);
  font-size: 13px;
  line-height: 1.7;
  color: var(--ts-label);
}

.form-panel {
  border: 1px solid var(--ts-hairline);
  border-radius: var(--ts-radius-panel);
  padding: var(--ts-space-6);
}
.form-panel-head {
  margin: 0 0 var(--ts-space-3);
  font: 500 13px var(--ts-font-mono);
  letter-spacing: 2px;
  color: var(--ts-paper-text);
}
.form-panel-body {
  margin: 0;
  font-size: var(--ts-text-body);
  color: var(--ts-paper-text);
}

.form-alert {
  margin: var(--ts-space-4) 0 0;
  font-size: 15px;
  color: var(--ts-rose);
}

/* --- 9. Footer ------------------------------------------------------------- */

.footer {
  border-top: 1px solid var(--ts-hairline);
  padding: var(--ts-space-7) 0 var(--ts-space-8);
}

.footer-logo { display: block; height: 20px; width: auto; }

.footer-positioning {
  margin: var(--ts-space-5) 0 0;
  font-size: var(--ts-text-small);
  color: var(--ts-mist);
  max-width: 72ch;
}

.footer-legal {
  margin: var(--ts-space-5) 0 0;
  font: 400 var(--ts-text-small)/1.9 var(--ts-font-mono);
  color: var(--ts-mist);
}
.footer-legal a {
  display: inline-block;
  padding: 10px 4px;
  margin: 0 -4px;
}

/* --- Responsive ------------------------------------------------------------ */

@media (max-width: 1000px) {
  .section { padding: var(--ts-space-8) 0; }
  .hero-grid { gap: var(--ts-space-6); }

  /* Four auto tracks in a 720px line give the certificates cell 191px and
     wrap its value to three lines. Two rows of two, with the interior
     hairlines redrawn for the second row. */
  .hero-cells { grid-template-columns: repeat(2, 1fr); }
  .hero-cells .cell-value { white-space: normal; }
  .hero-cells .cell:nth-child(3) { border-left: 0; }
  .hero-cells .cell:nth-child(n+3) { border-top: 1px solid var(--ts-hairline); }

  /* The label column is 215px of a 320px line here, so it takes the whole
     line instead and the value follows underneath. */
  .spec-row { grid-template-columns: 1fr; gap: var(--ts-space-2); }

  /* Between 721 and 1000px the text column is taller than it is wide, and a
     cover-fit frame would crop the sides of the image. Keep the frame's own
     5:4 ratio and let the columns top-align instead. */
  .hero-grid { align-items: start; }
  .hero-media { min-height: 0; }
  .hero-media > img { flex: none; aspect-ratio: 640 / 512; height: auto; }

  .steps { grid-template-columns: repeat(2, 1fr); }
  .operator-grid { gap: var(--ts-space-6); }
  .operator-media { grid-column: 1 / 7; }
  .operator-bio { grid-column: 7 / 13; }
  .aircraft-grid { gap: var(--ts-space-6); }
}

@media (max-width: 720px) {
  .section { padding: var(--ts-space-7) 0; }

  /* 64px pinned is ~10% of a 667px viewport. 54px still clears the 28px logo
     and the CTA, and the scroll-margin follows it automatically. */
  :root { --ts-nav-h: 54px; }

  /* 342px of usable bar at 390 wide. The narrower CTA is what keeps the
     logo off its max-width clamp. */
  .nav-cta { padding-left: 14px; padding-right: 14px; }
  .hero-grid { grid-template-columns: 1fr; }
  .hero-text, .hero-media { grid-column: 1 / -1; }
  .hero-media { min-height: 0; }
  .hero-media > img { flex: none; aspect-ratio: 640 / 512; height: auto; }
  .placeholder--hero { aspect-ratio: 640 / 512; }
  .hero-cta .ts-btn,
  .hero-cta .ts-btn-secondary,
  .sample-actions .ts-btn-secondary { width: 100%; }

  /* Clusters stack as full-width rows; the dividers become top borders. */
  .cell-cluster,
  .hero-cells,
  .tier-cells { grid-template-columns: 1fr; }
  .cell + .cell {
    border-left: 0;
    border-top: 1px solid var(--ts-hairline);
  }

  .datatable { padding: var(--ts-space-4) var(--ts-space-5); }
  .datatable-head,
  .datatable-row { grid-template-columns: 1fr; }
  .datatable-a { padding: var(--ts-space-3) 0 var(--ts-space-1); }
  .datatable-b {
    padding: 0 0 var(--ts-space-3);
    border-left: 0;
  }
  .datatable-head .datatable-b { padding-bottom: var(--ts-space-3); }

  .split-points { grid-template-columns: 1fr; gap: var(--ts-space-6); }
  .points-col + .points-col {
    padding-left: 0;
    border-left: 0;
  }

  .proof-grid { grid-template-columns: 1fr; }

  /* The label and its value keep one line each at 390px. */
  .hero-eyebrow { font-size: 12px; letter-spacing: 2px; }

  .steps { grid-template-columns: 1fr; gap: var(--ts-space-5); }

  .tier { grid-template-columns: 1fr; gap: var(--ts-space-4); }

  .operator-grid { grid-template-columns: 1fr; }
  .operator-media, .operator-bio { grid-column: 1 / -1; }
  .operator-photo { height: auto; aspect-ratio: 3 / 4; }
  .placeholder--headshot { aspect-ratio: 4 / 3; }

  .aircraft-grid { grid-template-columns: 1fr; }
  .aircraft-media, .aircraft-specs { grid-column: 1 / -1; }
}
