/* Skeg Software — Midnight & Marker, "chart & instrument".
   A skeg is a precision part, so the nautical language here is marine-chart and
   chart-instrument: contours, bearings, soundings, hairline plates. Not surf shop.

   The organising idea: everything in the water curves, and one line does not.
   The contour field drifts, section rules bend the eye across the page, and the
   fixed course line holds dead straight through all of it. The brand argument is
   made by the layout rather than asserted in the copy.

   Design-library patterns applied (Desktop/AI-Projects/_design-library):
     01 named-easing-tokens ...... two curves, three durations, nothing bespoke
     02 fluid-type-scale ......... one base, clamp() steps, zero size overrides
     03 additive-motion .......... motion only ADDED under no-preference
     04 reserved-accent .......... accent held near ~1% of painted area
     05 depth-without-shadows .... zero box-shadow; depth from ground + hairline
     06 colour-rooms ............. sections re-scope --room-* tokens
     09 case-weight-hierarchy .... mono/uppercase/tracking carries rank, not size
     21 native-focus-ring ........ :focus-visible enhanced, never removed
     29 hairline-plate-button .... 1px unfilled plate that inverts on hover
   Fonts are declared in fonts.css (self-hosted + metric-matched fallbacks). */

/* ============================================================
   1. TOKENS
   ============================================================ */

:root {
  /* -- ground & ink ------------------------------------------------ */
  --bg: #0a0f1e;
  --bg-raised: #121a30;
  --bg-inset: #0e1526;
  --line: rgba(234, 238, 247, 0.1);
  --line-strong: rgba(234, 238, 247, 0.16);
  --fg: #eaeef7;
  --fg-muted: #96a2bd;
  --accent: #8a9461;
  --accent-bright: #a8b37a;
  /* Was #64789c, which measured 4.32:1 on --bg and FAILED WCAG AA for the small
     text it used to carry (footer legal, product labels). Lifted twice: the
     first correction cleared --bg but still measured 4.39:1 against the LIGHTEST
     ground it lands on (--bg-raised plates). A muted token has to pass on the
     lightest surface it is used on, not the darkest — this value clears 4.75:1
     there and 5.3:1 on --bg. */
  --steel: #7387ab;

  /* -- room tokens (pattern 06) ------------------------------------
     Sections re-declare these, so one component re-themes itself by
     context instead of every section shipping its own rules. */
  --room-bg: var(--bg);
  --room-line: var(--line);
  --room-plate: var(--bg-raised);

  /* -- type scale (pattern 02) -------------------------------------
     One base, ~1.25 → 1.333 ratio. No component overrides a font-size
     with a raw px/rem value; they reference a step. */
  --step--2: clamp(0.69rem, 0.68rem + 0.06vw, 0.73rem);
  --step--1: clamp(0.83rem, 0.81rem + 0.11vw, 0.89rem);
  --step-0: clamp(1rem, 0.97rem + 0.17vw, 1.09rem);
  --step-1: clamp(1.2rem, 1.14rem + 0.29vw, 1.37rem);
  --step-2: clamp(1.44rem, 1.34rem + 0.48vw, 1.71rem);
  --step-3: clamp(1.73rem, 1.57rem + 0.78vw, 2.14rem);
  --step-4: clamp(2.07rem, 1.83rem + 1.22vw, 2.67rem);
  --step-5: clamp(2.49rem, 2.11rem + 1.88vw, 3.34rem);
  --step-6: clamp(2.99rem, 2.42rem + 2.83vw, 4.18rem);

  /* -- space ------------------------------------------------------- */
  --space-2xs: 0.375rem;
  --space-xs: 0.75rem;
  --space-s: 1.125rem;
  --space-m: 1.75rem;
  --space-l: clamp(2.5rem, 1.9rem + 3vw, 4rem);
  --space-xl: clamp(4rem, 2.6rem + 7vw, 7.5rem);

  /* -- motion (pattern 01) -----------------------------------------
     Two curves cover the whole site. --ease-swell is the corpus-converged
     standard curve (two award sites landed on it independently); --ease-set
     is the decisive settle used where something arrives and stays put. */
  --ease-swell: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-set: cubic-bezier(0.24, 1, 0.36, 1);
  /* A third curve, added for one job the other two cannot do: both of them end
     at zero velocity, and the voyage has to arrive and KEEP GOING. This one
     decelerates to the cruise rate (~34 units/s) instead of to a stop, so the
     hand-off from arrival to the endless loop has no visible gear change.
     Named and documented rather than inlined — see pattern 01. */
  --ease-cruise: cubic-bezier(0.16, 0.45, 0.72, 0.91);
  --dur-quick: 160ms;
  --dur-base: 320ms;
  --dur-slow: 720ms;

  /* -- measure & shape --------------------------------------------- */
  --radius: 10px;
  --max: 68rem;
  --gutter: clamp(1.25rem, 0.9rem + 1.6vw, 2rem);
  --hair: 1px;

  /* Fallbacks only — fonts.css redefines these with self-hosted faces
     and metric-matched fallback families. */
  --sans: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
  --display: "Space Grotesk", "Segoe UI", system-ui, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", monospace;
}

/* ============================================================
   2. BASE
   ============================================================ */

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* The fixed header must not cover an anchored section heading. */
  scroll-padding-top: 5rem;
}

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--sans);
  font-size: var(--step-0);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

::selection {
  background: var(--accent);
  color: var(--bg);
}

a {
  color: inherit;
  text-decoration: none;
}

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

/* Pattern 21: enhance the focus ring, never remove it. Accent on midnight
   measures ~8:1, so the indicator is louder than the thing it marks. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

.skipLink {
  position: absolute;
  left: var(--gutter);
  top: -100%;
  z-index: 100;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  background: var(--accent);
  color: var(--bg);
  font-family: var(--mono);
  font-size: var(--step--1);
  padding: 0.7rem 1.1rem;
  border-radius: 0 0 var(--radius) var(--radius);
}

.skipLink:focus {
  top: 0;
}

h1,
h2,
h3 {
  font-family: var(--display);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
  text-wrap: balance;
}

/* Pattern 09: rank carried by family, case and tracking — not by size.
   Every label on the page is the same small mono size; what changes is
   colour and context. That is what makes the display type feel large. */
.eyebrow,
.productLabel,
.plateIdx,
.readout dt,
.chart__bearing {
  font-family: var(--mono);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.eyebrow {
  color: var(--accent);
  display: flex;
  align-items: baseline;
  gap: 0.7rem;
}

/* Chart-style section index. Reads as an instrument label, and gives the
   eyebrow a second rank without introducing a second size. */
.idx {
  color: var(--steel);
  letter-spacing: 0.1em;
}

.idx::after {
  content: "";
  display: inline-block;
  width: 1.6rem;
  height: 1px;
  background: var(--line-strong);
  vertical-align: middle;
  margin-left: 0.7rem;
}

.inner {
  max-width: var(--max);
  margin: 0 auto;
  padding: var(--space-xl) var(--gutter);
  position: relative;
  z-index: 1;
}

/* Waterline. A section boundary is a hairline with a single tick on it,
   the way a chart marks a depth change. */
section + section {
  border-top: var(--hair) solid var(--room-line);
  position: relative;
}

section + section::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 0;
  width: clamp(2rem, 8vw, 5rem);
  height: 2px;
  background: var(--accent);
  opacity: 0.55;
}

/* ============================================================
   3. CHART FURNITURE
   The fixed layer the page scrolls past. Decorative, aria-hidden,
   pointer-events:none — it can never intercept a click or a tab stop.
   ============================================================ */

.chart {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}

/* Bathymetric contours — one SVG, no image request, sitting under everything
   (design-library "fixed topographic watermark layer"). Kept under 8% opacity
   so it never competes with body copy for contrast. */
.chart__contours {
  position: absolute;
  inset: -12% -6% -6%;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 900' preserveAspectRatio='xMidYMid slice'%3E%3Cg fill='none' stroke='%236d81a6' stroke-opacity='0.5'%3E%3Cpath d='M-60 300 C 220 232, 470 356, 700 288 S 1080 176, 1260 232'/%3E%3Cpath d='M-60 356 C 220 288, 470 412, 700 344 S 1080 232, 1260 288'/%3E%3Cpath d='M-60 412 C 220 344, 470 468, 700 400 S 1080 288, 1260 344'/%3E%3Cpath d='M-60 500 C 260 440, 520 548, 760 488 S 1100 388, 1260 436'/%3E%3Cpath d='M-60 556 C 260 496, 520 604, 760 544 S 1100 444, 1260 492'/%3E%3Cpath d='M-60 700 C 300 640, 560 748, 800 688 S 1120 596, 1260 640'/%3E%3Cpath d='M900 -60 C 900 140, 1010 268, 1260 330'/%3E%3Cpath d='M980 -60 C 980 160, 1090 292, 1260 356'/%3E%3C/g%3E%3C/svg%3E");
  background-size: cover;
  opacity: 0.075;
}

/* Printed-chart grain. A chart is an ink-on-paper object, and a perfectly
   clean gradient never reads as one. One inline turbulence filter, no image
   request, held low enough that it costs nothing in contrast (re-audited at
   30/30 AA with it on). */
.chart__grain {
  position: absolute;
  inset: 0;
  opacity: 0.028;
  /* 64px tile at ONE octave. The first version of this used a 160px tile at
     three octaves and cost 2.3 SECONDS of LCP — feTurbulence rasterisation
     scales with area x octaves, and it was blocking first paint. Texture this
     subtle does not need the extra octaves; measured back at ~750ms LCP. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='1' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='64' height='64' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 64px 64px;
  /* Keeps the tile on its own layer so it is rasterised once, not on scroll. */
  will-change: transform;
}

/* The course line: dead straight, fixed, indifferent to the drift behind it. */
.chart__course {
  position: absolute;
  top: 0;
  bottom: 0;
  /* Sits in the left gutter, hugging the content column on wide screens. */
  left: clamp(0.9rem, calc((100vw - var(--max)) / 2 - 1.4rem), 4rem);
  width: 1px;
  background: linear-gradient(
    to bottom,
    transparent 0,
    var(--line-strong) 6rem,
    var(--line-strong) calc(100% - 6rem),
    transparent 100%
  );
}

/* Tick marks along the course line, as on a bearing scale. */
.chart__course::before {
  content: "";
  position: absolute;
  left: -3px;
  top: 6rem;
  bottom: 6rem;
  width: 7px;
  background-image: repeating-linear-gradient(
    to bottom,
    var(--line-strong) 0 1px,
    transparent 1px 56px
  );
  opacity: 0.9;
}

/* An accent segment that reads as the current heading mark. */
.chart__course::after {
  content: "";
  position: absolute;
  left: -1px;
  top: 42%;
  width: 3px;
  height: 4.5rem;
  background: var(--accent);
  opacity: 0.75;
}

/* Track laid so far — an accent rule that grows down the course line with scroll
   depth. Driven by a scroll-progress timeline, so there is no scroll handler
   and no jank; browsers without it simply never show it. */
.chart__progress {
  position: absolute;
  left: -1px;
  top: 6rem;
  width: 3px;
  height: calc(100% - 12rem);
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--accent) 35%, transparent),
    var(--accent)
  );
  transform: scaleY(0);
  transform-origin: top;
  opacity: 0.5;
}

.chart__bearing {
  position: absolute;
  top: calc(42% - 1.6rem);
  left: 0.65rem;
  font-size: var(--step--2);
  letter-spacing: 0.18em;
  color: var(--steel);
  white-space: nowrap;
}

/* Chart-frame corner ticks. Four hairlines that frame the viewport the way a
   chart sheet is trimmed — the "fixed corner frame" idea, reduced to marks. */
.chart__tick {
  position: absolute;
  width: 14px;
  height: 14px;
  border: 0 solid var(--line-strong);
}

.chart__tick--tl {
  top: 4.6rem;
  left: 1rem;
  border-top-width: 1px;
  border-left-width: 1px;
}
.chart__tick--tr {
  top: 4.6rem;
  right: 1rem;
  border-top-width: 1px;
  border-right-width: 1px;
}
.chart__tick--bl {
  bottom: 1rem;
  left: 1rem;
  border-bottom-width: 1px;
  border-left-width: 1px;
}
.chart__tick--br {
  bottom: 1rem;
  right: 1rem;
  border-bottom-width: 1px;
  border-right-width: 1px;
}

/* ============================================================
   4. HEADER
   ============================================================ */

.header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: var(--hair) solid var(--line);
}

.headerInner {
  max-width: var(--max);
  margin: 0 auto;
  padding: 0.85rem var(--gutter);
  display: flex;
  align-items: center;
  gap: var(--space-m);
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  min-height: 44px;
  font-family: var(--display);
  font-weight: 600;
  font-size: var(--step-0);
  letter-spacing: -0.01em;
}

.brand img {
  width: 26px;
  height: 26px;
}

.nav {
  display: flex;
  gap: var(--space-m);
  margin-left: auto;
  font-size: var(--step--1);
  font-weight: 500;
  color: var(--fg-muted);
}

/* 44px minimum touch target — the previous build measured four hits under it. */
.nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.nav a:hover {
  color: var(--fg);
}

.navToggle {
  display: none;
  margin-left: auto;
  min-height: 44px;
  background: none;
  border: var(--hair) solid var(--line-strong);
  border-radius: 8px;
  color: var(--fg);
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 0.4rem 0.9rem;
  cursor: pointer;
}

/* ============================================================
   5. HERO
   ============================================================ */

.hero .inner {
  padding-top: clamp(4.5rem, 3rem + 7vw, 8rem);
  padding-bottom: clamp(4.5rem, 3rem + 7vw, 8rem);
}

.heroKicker {
  margin-bottom: var(--space-s);
}

.hero h1 {
  font-size: var(--step-6);
  font-weight: 700;
  max-width: 15em;
}

.hero h1 em {
  font-style: normal;
  color: var(--accent);
}

.heroSub {
  margin-top: var(--space-s);
  max-width: 34rem;
  font-size: var(--step-1);
  color: var(--fg-muted);
  text-wrap: pretty;
}

/* Instrument readout. Three facts in a mono rail, the way a chart plotter
   shows heading and speed — carries the theme without another graphic. */
.readout {
  margin-top: var(--space-l);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-m);
  border-top: var(--hair) solid var(--line);
  padding-top: var(--space-s);
  max-width: 34rem;
}

.readout dt {
  color: var(--steel);
  margin-bottom: 0.3rem;
}

.readout dd {
  font-family: var(--display);
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--fg);
}

/* ============================================================
   6. STUDIO  (colour room: inset ground)
   ============================================================ */

.studio {
  --room-bg: var(--bg-inset);
  --room-plate: var(--bg);
  background: var(--room-bg);
}

.studioLede {
  font-family: var(--display);
  font-size: var(--step-3);
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.015em;
  max-width: 26em;
  margin-top: var(--space-s);
  text-wrap: pretty;
}

.studioLede em {
  font-style: normal;
  color: var(--accent);
}

.studioBody {
  margin-top: var(--space-m);
  max-width: 40rem;
  color: var(--fg-muted);
  font-size: var(--step-0);
}

/* -- the voyage: the metaphor, sailed ----------------------------
   Resting state is a MID-VOYAGE picture — the fin station-keeping at the head
   of the record it has laid, open water ahead of it. That is what a
   reduced-motion or no-JS visitor gets, and it is complete on its own. The
   animation in section 11 sails the fin in and then never stops: it is the
   chart that moves, not the course.

   Geometry, in viewBox units, all of it interlocking — change one and check
   the rest:
     fin station .... x 400, so the mark it lays sits at 414
     wake window .... right edge 414 (the fin); everything right of it is
                      water the fin has not reached yet
     marks .......... every 102, tile of 10 = a period of exactly 1020
     loop ........... one tile per 30s = ~34 units/s, a new sounding ~every 3s */

.voyage {
  margin-top: var(--space-l);
  border: var(--hair) solid var(--line);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  padding: clamp(1rem, 0.5rem + 2vw, 2rem);
  overflow: hidden;
}

.voyage svg {
  width: 100%;
  height: auto;
}

.v-swell {
  stroke: var(--steel);
  opacity: 0.5;
  stroke-dasharray: 3 5;
}

.v-chop path {
  stroke: var(--steel);
  opacity: 0.22;
  stroke-dasharray: 2 22;
}

.v-track {
  stroke: var(--accent);
  stroke-width: 2;
}

.v-finBody {
  fill: var(--accent);
}

/* The marks are two <use> copies of one tile, so their paint has to be
   INHERITED — a class selector will not reliably match inside a <use> shadow
   tree. Hence stroke-opacity/fill-opacity rather than opacity (opacity does not
   inherit), and `fill="none"` / `stroke="none"` in the markup so the ticks are
   not filled and the soundings are not outlined. */
.v-tile {
  stroke: var(--accent);
  stroke-width: 1;
  stroke-opacity: 0.6;
  fill: var(--steel);
  fill-opacity: 0.75;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-anchor: middle;
}

/* Wake trails sit behind the fin and are invisible at rest — they only mean
   anything while it is moving. */
.v-finWake {
  stroke: var(--accent);
  stroke-width: 1.5;
  opacity: 0;
  stroke-linecap: round;
}

.v-finWake--2 {
  stroke-width: 1;
}

/* At rest the fin is mid-voyage, station-keeping at the head of its own record.
   There is no "finished" state any more — that is the point of the part. */
.v-finTravel {
  transform: translate(400px, 132px);
}

/* -- principles: instrument plates ------------------------------- */

.principles {
  margin-top: var(--space-l);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-s);
  list-style: none;
}

.principles li {
  border-top: 2px solid var(--accent);
  padding-top: var(--space-xs);
}

.plateIdx {
  color: var(--steel);
  display: block;
  margin-bottom: var(--space-2xs);
}

.principles h3 {
  font-size: var(--step-1);
}

.principles p {
  margin-top: var(--space-2xs);
  color: var(--fg-muted);
  font-size: var(--step--1);
  line-height: 1.65;
}

/* ============================================================
   7. PRODUCTS
   ============================================================ */

.sectionHead {
  margin-bottom: var(--space-l);
}

.sectionHead h2 {
  font-size: var(--step-4);
  margin-top: var(--space-xs);
}

.productGrid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-s);
}

/* Pattern 05 — depth without shadows. There is no box-shadow anywhere in this
   stylesheet. A plate reads as raised because its ground differs from the room
   ground and a hairline closes it, which is also why it survives dark mode,
   print, and forced-colours without special casing. */
.productCard,
.productSoon {
  border: var(--hair) solid var(--line);
  border-radius: var(--radius);
  padding: clamp(1.25rem, 0.9rem + 1.6vw, 2rem);
  background: var(--room-plate);
}

.productCard {
  display: block;
  position: relative;
}

.productSoon {
  background: transparent;
  border-style: dashed;
  border-color: var(--line-strong);
  display: grid;
  align-content: center;
}

.productLabel {
  color: var(--steel);
}

.productCard h3,
.productSoon h3 {
  font-size: var(--step-2);
  margin-top: var(--space-xs);
}

.productSoon h3 {
  color: var(--fg-muted);
}

.productCard p,
.productSoon p {
  margin-top: var(--space-2xs);
  color: var(--fg-muted);
  font-size: var(--step--1);
  line-height: 1.65;
}

.productCard p strong {
  color: var(--fg);
  font-weight: 600;
}

.productSoon p {
  color: var(--steel);
}

.productLink {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  margin-top: var(--space-m);
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
}

/* Duplicate-label hover roll. The clipped box is sized by the first copy; the
   second sits exactly one box below and both translate up together on hover. */
.roll {
  display: inline-block;
  position: relative;
  overflow: hidden;
  vertical-align: bottom;
}

.roll__a,
.roll__b {
  display: block;
}

.roll__b {
  position: absolute;
  top: 100%;
  left: 0;
  white-space: nowrap;
}

/* ============================================================
   8. CONTACT
   ============================================================ */

.contact {
  --room-bg: var(--bg-inset);
  background: var(--room-bg);
}

.contact .inner {
  text-align: center;
}

.contact .eyebrow {
  justify-content: center;
}

.contact h2 {
  font-size: var(--step-4);
  margin-top: var(--space-xs);
}

.contact > .inner > p {
  margin: var(--space-xs) auto 0;
  max-width: 32rem;
  color: var(--fg-muted);
  text-wrap: pretty;
}

/* Pattern 29 — hairline plate button. An unfilled 1px plate that inverts on
   hover, which reads as engraved metal rather than a web button, and keeps the
   accent budget down (a filled block was the largest accent area on the
   previous build). */
.contactBtn {
  display: inline-block;
  margin-top: var(--space-l);
  border: var(--hair) solid var(--accent);
  border-radius: 4px;
  color: var(--accent);
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 1rem 2rem;
  min-height: 44px;
}

/* ============================================================
   9. FOOTER
   ============================================================ */

.footer {
  border-top: var(--hair) solid var(--line);
}

.footerInner {
  max-width: var(--max);
  margin: 0 auto;
  padding: var(--space-l) var(--gutter);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-s);
  position: relative;
  z-index: 1;
}

.footerBrand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--display);
  font-weight: 600;
  font-size: var(--step--1);
}

.footerBrand img {
  width: 22px;
  height: 22px;
}

.footerNav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-m);
  margin-left: auto;
  font-size: var(--step--1);
  color: var(--fg-muted);
}

.footerNav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.footerNav a:hover {
  color: var(--fg);
}

.footerLegal {
  width: 100%;
  font-size: var(--step--1);
  color: var(--steel);
}

/* ============================================================
   10. RESPONSIVE
   ============================================================ */

@media (max-width: 60rem) {
  .productGrid,
  .principles {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 47.5rem) {
  /* The course line needs gutter room it does not have on a phone; the
     section ticks carry the same language at this width. */
  .chart__course,
  .chart__tick {
    display: none;
  }

  .nav {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    gap: 0;
    background: var(--bg-raised);
    border-bottom: var(--hair) solid var(--line-strong);
    padding: 0.25rem 0;
  }

  .nav.open {
    display: flex;
  }

  .nav a {
    padding: 0.6rem var(--gutter);
  }

  .navToggle {
    display: block;
  }

  .headerInner {
    position: relative;
  }
}

/* ============================================================
   11. MOTION  (pattern 03 — ADDITIVE)
   Everything above is the resting layout, and it is complete: no transform is
   applied at rest, so nothing has to be "switched off" for reduced motion.
   Motion is only ever ADDED inside this query. That ordering also avoids the
   trap where flattening a resting transform MANUFACTURES movement.
   ============================================================ */

@media (prefers-reduced-motion: no-preference) {
  /* ---- scroll-triggered reveal -------------------------------------
     Gated on BOTH .js-reveal and this query, so content is visible by
     default in every other case. A reveal that hides content when its
     observer never fires is the most common way this pattern ships
     broken — main.js also force-reveals after 2.5s as a backstop. */
  .js-reveal [data-reveal] {
    opacity: 0;
    transform: translateY(14px);
    transition:
      opacity var(--dur-slow) var(--ease-swell),
      transform var(--dur-slow) var(--ease-set);
    transition-delay: calc(var(--i, 0) * 70ms);
  }

  /* Cards deal out with a slight rotation, so a row of plates arrives as a
     hand rather than as three identical fades. */
  .js-reveal [data-fan] {
    transform: translateY(26px) rotate(-1.4deg) scale(0.985);
    transform-origin: 50% 120%;
  }

  .js-reveal [data-reveal].is-in {
    opacity: 1;
    transform: none;
  }

  /* ---- hero headline: each word rises out of its own clipped box ---- */
  .js-reveal .riseWords .w > span {
    transform: translateY(115%);
    transition: transform 900ms var(--ease-set);
    transition-delay: calc(160ms + var(--i, 0) * 65ms);
  }

  .js-reveal .riseWords.is-in .w > span {
    transform: none;
  }

  /* ---- the voyage ---------------------------------------------------
     Two phases, and the seam between them is the whole trick.

     ARRIVAL (--vg-arrive): the fin sails in from off-canvas and the wake
     window travels with it, so the track, the marks and the soundings are
     printed exactly under the fin. Both share one duration AND one easing —
     different curves would desync the line from the thing drawing it.

     CRUISE (--vg-cruise, forever): the fin holds station and the record
     scrolls out from under it instead, looping by exactly one tile so the
     repeat has no seam. Nothing here ever lands.

     The hand-off is invisible because --ease-cruise decelerates the arrival to
     the cruise rate rather than to a stop: at the moment the fin parks, the
     ground is already moving at the speed the fin was. What changes is the
     camera, not the boat. */
  .voyage {
    --vg-arrive: 4.2s;
    --vg-cruise: 30s;
  }

  .js-reveal .voyage .v-finTravel {
    transform: translate(-30px, 132px);
  }

  .js-reveal .voyage.is-in .v-finTravel {
    animation: finTravel var(--vg-arrive) var(--ease-cruise) forwards;
  }

  /* The window starts closed to the left of the frame — nothing is drawn until
     the fin has been there. */
  .js-reveal .voyage .v-wakeClip {
    transform: translateX(-430px);
  }

  .js-reveal .voyage.is-in .v-wakeClip {
    animation: openWake var(--vg-arrive) var(--ease-cruise) forwards;
  }

  .js-reveal .voyage.is-in .v-marks {
    animation: chartScroll var(--vg-cruise) linear var(--vg-arrive) infinite;
  }

  /* The fin is under way permanently now, so the wake fades up and stays up. */
  .js-reveal .voyage.is-in .v-finWake {
    animation: wakeIn 700ms linear 350ms forwards;
  }

  /* An SVG that never stops is an SVG that never stops repainting. main.js
     flags the panel .is-live only while it is on screen; off screen everything
     in here parks. Gated on .js-reveal so a blocked script leaves the panel
     running rather than frozen. */
  .js-reveal .voyage:not(.is-live) .v-swell,
  .js-reveal .voyage:not(.is-live) .v-chop path,
  .js-reveal .voyage:not(.is-live) .v-finBob,
  .js-reveal .voyage:not(.is-live) .v-finTravel,
  .js-reveal .voyage:not(.is-live) .v-marks,
  .js-reveal .voyage:not(.is-live) .v-wakeClip {
    animation-play-state: paused;
  }

  /* Pointer-reactive water: the currents lean away from the cursor, so the
     panel feels like a surface rather than a picture. Fine pointers only —
     there is no hover on touch, and --mx/--my simply stay 0 there. */
  @media (pointer: fine) {
    .v-water,
    .v-chop {
      transition: transform 900ms var(--ease-swell);
      transform: translate3d(calc(var(--mx, 0) * -10px), calc(var(--my, 0) * -6px), 0);
    }

    .v-finTravel {
      transition: transform 900ms var(--ease-swell);
    }
  }

  /* Waterline sweep: the accent tick at each section boundary draws across as
     that boundary comes into view. Progress-linked, so it tracks the scroll
     rather than firing on a timer. */
  @supports (animation-timeline: view()) {
    section + section::before {
      transform: scaleX(0);
      transform-origin: left;
      animation: waterline linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 22%;
    }

    @keyframes waterline {
      to { transform: scaleX(1); }
    }
  }

  /* Course-line progress, driven by document scroll position. */
  @supports (animation-timeline: scroll()) {
    .chart__progress {
      animation: courseProgress linear both;
      animation-timeline: scroll(root block);
    }

    @keyframes courseProgress {
      to { transform: scaleY(1); }
    }
  }

  /* Label roll on hover AND on keyboard focus — a hover-only affordance is
     invisible to anyone tabbing through. */
  .roll__a,
  .roll__b {
    transition: transform var(--dur-base) var(--ease-set);
  }

  .productCard:hover .roll__a,
  .productCard:focus-visible .roll__a,
  .productCard:hover .roll__b,
  .productCard:focus-visible .roll__b {
    transform: translateY(-100%);
  }

  /* The fin keeps riding the swell after it arrives — it is station-keeping,
     not switched off. That is the whole point of the part. */
  .v-finBob {
    animation: bob 2.7s var(--ease-swell) infinite;
  }

  /* Water never stops. Each current carries its own negative delay so the
     three are permanently out of phase. */
  .v-swell {
    animation: swellDrift 9s var(--ease-swell) infinite alternate;
    animation-delay: var(--p, 0s);
  }

  .v-chop path {
    animation: chop 6s linear infinite;
    animation-delay: var(--p, 0s);
  }

  @keyframes finTravel {
    from { transform: translate(-30px, 132px); }
    to   { transform: translate(400px, 132px); }
  }

  /* Same 430 units as the fin's travel, on the same curve — the window's right
     edge and the fin are the same point moving. */
  @keyframes openWake {
    from { transform: translateX(-430px); }
    to   { transform: translateX(0); }
  }

  /* Exactly one tile. Any other distance shows the seam. */
  @keyframes chartScroll {
    from { transform: translateX(0); }
    to   { transform: translateX(-1020px); }
  }

  @keyframes wakeIn {
    from { opacity: 0; }
    to   { opacity: 0.5; }
  }

  @keyframes bob {
    0%, 100% { transform: translateY(0) rotate(0deg); }
    50%      { transform: translateY(-3px) rotate(-1.2deg); }
  }

  @keyframes swellDrift {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(-14px, 5px, 0); }
  }

  @keyframes chop {
    from { stroke-dashoffset: 0; }
    to   { stroke-dashoffset: -48; }
  }

  .nav a,
  .footerNav a,
  .productLink,
  .brand {
    transition: color var(--dur-quick) var(--ease-swell);
  }

  .productCard,
  .contactBtn {
    transition:
      border-color var(--dur-base) var(--ease-swell),
      background-color var(--dur-base) var(--ease-swell),
      color var(--dur-base) var(--ease-swell);
  }

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

  .productCard:hover .productLink {
    color: var(--accent-bright);
  }

  .arrow {
    display: inline-block;
    transition: transform var(--dur-base) var(--ease-set);
  }

  .productCard:hover .arrow {
    transform: translateX(4px);
  }

  .contactBtn:hover,
  .contactBtn:focus-visible {
    background: var(--accent);
    color: var(--bg);
  }

  /* Ambient swell. A 90-second drift on the contour field only — slow enough
     to read as "alive" rather than as animation, and it touches nothing that
     carries text. Composited transform only, so it costs no layout. */
  .chart__contours {
    animation: swell 90s var(--ease-swell) infinite alternate;
    will-change: transform;
  }

  @keyframes swell {
    from {
      transform: translate3d(0, 0, 0);
    }
    to {
      transform: translate3d(-1.5%, 1.2%, 0);
    }
  }
}

/* Reduced-motion users still scroll and still hover; they simply get the
   resting layout with instant state changes. Smooth scrolling is the one
   thing that must be actively undone, because it is a UA behaviour rather
   than something this stylesheet adds. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

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

  .contactBtn:hover,
  .contactBtn:focus-visible {
    background: var(--accent);
    color: var(--bg);
  }
}

/* Windows high-contrast / forced-colours: the chart furniture is decoration
   and should simply leave. */
@media (forced-colors: active) {
  .chart {
    display: none;
  }
}

/* ============================================================
   WORK
   Four client sites rather than the two product plates, so the
   grid goes to a comfortable two-up on desktop and stacks on
   mobile. Cards reuse .productCard wholesale — the plate, the
   hairline and the rolling link are already the house pattern,
   and a second near-identical card style would be two things to
   keep in sync for no visual gain.
   ============================================================ */
.workGrid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (max-width: 720px) {
  .productGrid,
  .workGrid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ============================================================
   HERO HEADLINE — the box model the rise animation needs

   The transform/transition rules for .riseWords live in the
   reduced-motion block below, but they were a no-op without
   these: `transform` has NO effect on a non-replaced inline
   element, and both the word wrapper and the inner span default
   to display:inline. The transition still reported itself as
   registered (transform / 0.9s), which is exactly why this went
   unnoticed — the animation was configured, measurable, and dead.

   These are structural, so they sit OUTSIDE the motion query:
   the clipped box has to exist whether or not the words move.
   ============================================================ */
.riseWords .w {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  /* The clip would otherwise cut the descenders in "keep" and
     "you"; pad the box and pull the same amount back off the
     layout so nothing shifts. */
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
}

.riseWords .w > span {
  display: inline-block;
}


/* ============================================================
   WORK INDEX — numbered rows with a synced preview

   Ported from the standalone portfolio so the work reads the way
   it did there: hover a row, or scroll it into the band, and the
   preview beside it swaps to that site. Every token is bridged
   onto this stylesheet's own palette, type and easing, so it
   inherits the chart-instrument language rather than dragging a
   second design system in behind it.

   ORDER IS LOAD-BEARING: the <picture> elements inside .slot are
   paired to the .row items by POSITION. Reorder one, reorder both.
   ============================================================ */
/* ── 7. Work index ──────────────────────────────────────────────────────── */

.index {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}

.index__list { grid-column: 1; grid-row: 1; list-style: none; margin: 0; padding: 0; }

.index__media { grid-column: 2; grid-row: 1; position: sticky; top: calc(64px + 2rem); }

.slot {
  position: relative;
  aspect-ratio: 1520 / 950;
  overflow: hidden;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--bg-raised);
}

.shot { position: absolute; inset: 0; opacity: 0; }

.shot img { width: 100%; height: 100%; object-fit: cover; object-position: top center; }

.shot.is-active { opacity: 1; }

.slot__cap {
  font-family: var(--mono);
  font-size: var(--step--2);
  letter-spacing: .04em;
  color: var(--fg-muted);
  margin: .9rem 0 0;
  padding-left: 1.1rem;
  position: relative;
}

.slot__cap::before {
  content: "";
  position: absolute;
  left: 0; top: .5em;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--accent);
}

.row {
  position: relative;
  padding: clamp(1.5rem, 3vw, 2.1rem) 0 clamp(1.6rem, 3vw, 2.2rem) clamp(1rem, 2vw, 1.75rem);
  border-top: 1px solid var(--line);
}

.row:last-child { border-bottom: 1px solid var(--line); }

.row::before {
  /* active marker — a rule that grows down the left edge */
  content: "";
  position: absolute;
  left: 0; top: -1px;
  width: 2px; height: calc(100% + 1px);
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform var(--dur-base) var(--ease-set);
}

.row.is-active::before { transform: scaleY(1); }

.row__hit {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: .5rem 1rem;
  text-decoration: none;
  color: inherit;
}

.row__num {
  font-family: var(--mono);
  font-size: var(--step--2);
  color: var(--fg-muted);
  letter-spacing: .1em;
  transition: color var(--dur-base) var(--ease-set);
}

.row.is-active .row__num { color: var(--accent); }

.row__name {
  font-family: var(--display);
  font-size: var(--step-4);
  line-height: 1;
  letter-spacing: -.02em;
  transition: color var(--dur-base) var(--ease-set);
}

.row.is-active .row__name { color: var(--accent); }

.row__kind {
  grid-column: 2;
  font-family: var(--mono);
  font-size: var(--step--2);
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--fg-muted);
}

.row__go {
  font-size: var(--step-1);
  color: var(--fg-muted);
  grid-column: 3;          /* explicit — an auto-placed item with a definite
                              row is placed before the auto-flow ones and
                              would otherwise claim column 1 */
  grid-row: 1 / span 2;
  align-self: center;
  transition: transform var(--dur-base) var(--ease-swell),
              color var(--dur-base) var(--ease-set);
}

.row.is-active .row__go { color: var(--accent); transform: translate(3px, -3px); }

.row__note {
  margin: 1rem 0 0;
  max-width: 52ch;
  font-size: var(--step--1);
  color: var(--fg-muted);
  transition: color var(--dur-base) var(--ease-set);
}

.row__note strong { color: var(--fg); font-weight: 600; }

.row.is-active .row__note { color: color-mix(in srgb, var(--fg) 78%, var(--fg-muted)); }

.row__stack {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin: 1rem 0 0;
  padding: 0;
}

.row__stack li {
  font-family: var(--mono);
  font-size: var(--step--2);
  letter-spacing: .04em;
  color: var(--fg-muted);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: .28rem .7rem;
}

@media (prefers-reduced-motion: no-preference) {
  html:not(.reduced-motion) .shot {
    transition: opacity .32s var(--ease-set);
  }

  html:not(.reduced-motion) .row__name,
  html:not(.reduced-motion) .cardx,
  html:not(.reduced-motion) .btn { will-change: auto; }
}

@media (max-width: 900px) {
  /* Media companion moves above the list and is driven by scroll proximity. */
  .index { grid-template-columns: 1fr; gap: 1.75rem; }

  .index__media {
    grid-column: 1; grid-row: 1;
    position: sticky;
    top: calc(64px + .75rem);
    z-index: 2;
    max-width: 100%;
  }

  .index__media .slot { box-shadow: 0 18px 40px -24px rgba(0, 0, 0, .8); }

  .index__list { grid-column: 1; grid-row: 2; }
}

@media (max-width: 640px) {
  .row__hit { grid-template-columns: auto 1fr; }

  .row__go { grid-row: 1; align-self: baseline; }
}


/* ============================================================
   HERO PLAN DRAWING

   Ported from the standalone portfolio: an orthographic plate
   with dimension lines, four corner nodes, SPEC / BUILD / SHIP
   labels, one accent dash walking the outline, and the surveyor
   dot sweeping the interior cross lines.

   Rebranded onto this site's tokens — the portfolio's literal
   hexes were sampled against ITS ground (#101A2E) and would sit
   wrong on Skeg's darker one, so lines and nodes are expressed
   as mixes of --steel and --bg, and the surveyor takes --accent.

   `html:not(.reduced-motion)` is dropped: that class does not
   exist here, and the enclosing motion query already gates it.
   ============================================================ */
.hero__plan {
  position: absolute;
  inset: 0;
  z-index: -1;
  color: color-mix(in srgb, var(--steel) 42%, transparent);
  pointer-events: none;
}

.hero__plan svg { width: 100%; height: 100%; }

.plan__draw { fill: none; stroke: color-mix(in srgb, var(--steel) 62%, transparent); stroke-width: 1.25; }

.pl--dim { stroke: var(--steel); }

.pl--lead { stroke: var(--steel); stroke-dasharray: 5 4; }

.nd { fill: var(--bg); stroke: #7D8CA8; stroke-width: 1.5; }

.nd--live { fill: var(--accent); stroke: var(--accent); }

/* Outline sweep. Rests at opacity 0, so with motion off or JS dead it adds
   nothing rather than sitting there as a stray mark. */
.tr {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2.5;
  stroke-linecap: round;
  opacity: 0;
}

.pl__label {
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: .22em;
  fill: var(--steel);
  /* Same value as `fill` so contrast tooling, which samples `color` on SVG
     text, reads the colour actually painted (5.12:1 on the hero ground)
     instead of the inherited grid colour. */
  color: var(--steel);
  stroke: none;
  text-anchor: middle;
}

.pl__label--start { text-anchor: start; }

@media (prefers-reduced-motion: no-preference) {
  /* Ambient idle on the plan drawing + a one-shot draw-in. */
  .plan__draw { animation: drift 26s var(--ease-set) infinite alternate; }

  /* The whole drawing must be finished inside ~1.35s. An earlier version ran
     1.9s of line-draw and only started the nodes at 900ms, so the hero was
     still assembling itself two seconds in — long enough that a quick scroll
     past it saw an unfinished drawing. */
  /* The leader is excluded: it carries its own 5/4 dash, which this rule's
     dasharray would otherwise overwrite and render solid. */
  .plan__draw .pl:not(.pl--lead) {
    stroke-dasharray: var(--len, 1400);
    stroke-dashoffset: 0;
    animation: draw 1.35s var(--ease-set) both;
  }

  .pl--lead { animation: fade .5s var(--ease-set) both .75s; }

  /* Outline sweep — a single short dash walking the plate edge. --len comes
     from JS, so the travel distance is exactly the path length and the loop
     has no seam. */
  .tr {
    opacity: 1;
    animation: trace 9s linear infinite 1.35s;
  }

  /* Infinite motion is an infinite repaint: park it once the hero is off
     screen rather than animating behind the rest of the page. */
  .hero__plan.is-idle .tr,
  .hero__plan.is-idle .plan__draw .nd--live,
  .hero__plan.is-idle .plan__draw { animation-play-state: paused; }

  .plan__draw .nd {
    animation: pop .5s var(--ease-swell) both;
    animation-delay: calc(480ms + var(--i, 0) * 90ms);
  }

  .plan__draw .nd { transform-box: fill-box; transform-origin: center; }

  /* THE SURVEYOR. The olive dot sweeps the plate's two INTERIOR cross lines —
     out along the horizontal, back through the crossing to its far end, home,
     then the same on the vertical — pausing briefly at each end. It is kept
     off the outline deliberately: that track belongs to the sweep above, and
     two accent marks orbiting the same rectangle read as clutter.

     Keyframe percentages are proportional to leg length
     (288 · 384 · 96 · 96 · 240 · 144 = 1248 units, 76% of the cycle) with a
     4% dwell at each of the six stops, so linear timing gives constant speed
     instead of visibly hurrying the short legs.

     Two things this selector has to get right: it must carry `.plan__draw` or
     the `.nd` pop rule above outranks it, and it must sit AFTER that rule to
     win the tie. It animates `translate`, not `transform`, so the sweep
     composes with the pop-in's scale instead of replacing it. */
  .plan__draw .nd--live {
    animation: pop .5s var(--ease-swell) both calc(480ms + var(--i, 0) * 90ms),
               survey 18s linear infinite 1.6s;
  }

  /* Pointer response — the plan notices the cursor. Capped at 10px. */
  .plan__draw {
    translate: calc(var(--px, 0) * 10px) calc(var(--py, 0) * 10px);
    transition: translate 1.1s var(--ease-set);
  }
}


@keyframes drift {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(-14px, 10px, 0); }
  }

@keyframes draw {
    from { stroke-dashoffset: var(--len, 1400); }
    to   { stroke-dashoffset: 0; }
  }

@keyframes fade { from { opacity: 0; } to { opacity: 1; } }

@keyframes trace {
    from { stroke-dashoffset: 0; }
    to   { stroke-dashoffset: calc(var(--len, 1240) * -1px); }
  }

@keyframes pop { from { opacity: 0; transform: scale(.4); } to { opacity: 1; transform: scale(1); } }

@keyframes survey {
    0%,     4%    { translate: 0 0; }        /* the crossing — home */
    21.5%,  25.5% { translate: 288px 0; }    /* horizontal, right end */
    48.9%,  52.9% { translate: -96px 0; }    /* horizontal, left end */
    58.7%,  62.7% { translate: 0 0; }        /* back through the crossing */
    68.5%,  72.5% { translate: 0 -96px; }    /* vertical, top */
    87.1%,  91.1% { translate: 0 144px; }    /* vertical, bottom */
    100%          { translate: 0 0; }
  }
