/*
  bisonsawmill.com — marketing site

  One stylesheet for all six pages. Nothing is loaded from anywhere else: no
  external font, no CDN reset. The Content-Security-Policy is default-src
  'self', so a remote asset would be blocked outright rather than degrading
  quietly.

  Two contexts share these components:

    body.home   the hero page — white nav over a video, no page chrome
    body        every other page — ink on paper, a compact nav, a content column

  The nav and the footer are one pattern in both, themed with custom
  properties rather than duplicated. Homepage-only rules are prefixed .home
  so that adding a page cannot change how the hero renders.
*/


/* ------------------------------------------------------------------ *
   Tokens
 * ------------------------------------------------------------------ */

:root {
  /*
    Neutral grays with one accent. The warm cream palette this replaced was
    chosen to sit with the photograph; the application is gray and navy, and
    the site should not look like a different product from the thing it sells.

    Nothing here is warm. --paper is a light gray page, --paper-raised is the
    white of a card sitting on it.
  */
  --ink: #15181d;
  --ink-soft: #5e666f;
  --paper: #f7f8fa;
  --paper-raised: #ffffff;
  --rule: #e0e4e9;

  /*
    The accent, used sparingly: the footer, the hub centre, the small
    uppercase labels, and every focus ring. Not for body text, not for
    headings — those stay ink.
  */
  --navy: #16264a;
  --navy-tint: rgba(22, 38, 74, 0.07);
  --navy-tint-strong: rgba(22, 38, 74, 0.12);
  --on-navy: #ffffff;

  /* Wordmark width in the compact nav on interior pages. .home overrides
     this with the hero's much larger setting. */
  --wordmark-width: clamp(150px, 18vw, 200px);

  /* Breathing room from the edges of the viewport. */
  --edge: clamp(20px, 4vw, 48px);

  /* The content column. Prose stays narrow; the diagram is allowed wider. */
  --measure: 34rem;
  --measure-wide: 60rem;

  /*
    Type. No web font: "no CDN dependencies" is a project rule, and a Google
    Fonts link would be blocked by the CSP regardless. This is the modern
    system stack — SF Pro on macOS and iOS, Segoe UI Variable on Windows,
    Roboto on Android, all clean neo-grotesques.
  */
  --font-sans: "Inter", system-ui, -apple-system, "Segoe UI Variable Text",
               "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial,
               sans-serif;

  /* Hamburger geometry. The bar-to-bar distance is gap + height, which the
     open-state transforms depend on — change these together. */
  --bar-width: 28px;
  --bar-height: 2px;
  --bar-gap: 7px;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  height: 100%;
  /* Anchors and skip links land below the fixed nothing; kept modest. */
  scroll-behavior: auto;
}

body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;

  font-family: var(--font-sans);
  color: var(--ink);
  background-color: var(--paper);

  /*
    Tabular figures everywhere. Numbers on this site are quantities and
    prices — they should align in a column and not shift width as they
    change. Every face in the stack above supports this.
  */
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;

  /*
    Light text on dark renders heavy and slightly muddy under macOS's default
    subpixel antialiasing. Greyscale smoothing keeps it crisp.
  */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}


/* ------------------------------------------------------------------ *
   Home: background video, still and scrim
 * ------------------------------------------------------------------ */

/*
  The still, sitting behind the video: first paint before the video has
  buffered, and the permanent picture anywhere the video does not play.

  backdrop.jpg (293 KB), not the original bisonsawmill.png (2.3 MB). With a
  2.28 MB video on the same page the PNG would have meant 4.6 MB for one
  screen. The <video> poster points at this same file, so it is fetched once
  and serves both jobs.

  Two things here are load-bearing, and both are about the footer having made
  the page taller than the viewport:

  It stays on body rather than moving to .hero, because a background on body
  propagates to the canvas and the canvas paints beneath everything. An
  in-flow element's background does not: CSS paints negative-z-index children
  BEFORE in-flow block backgrounds, so a background on .hero would have been
  drawn on top of the video at z-index -2 and hidden it completely.

  background-attachment: fixed, because `cover` sizes against the positioning
  area. Left scrolling, that area is now the whole body — hero plus footer —
  and the photograph would be scaled up and cropped harder than before.
  Fixed makes the area the viewport again, which is exactly what body's own
  box used to be, so the crop on the first screen is unchanged.
*/
body.home {
  background-color: #101822;
  background-image: url("images/backdrop.jpg");
  background-repeat: no-repeat;
  background-position: center center;
  background-size: cover;
  background-attachment: fixed;
}

/*
  Stacking on the home page, bottom to top:

    canvas         background-color and backdrop.jpg
    z-index: -2    the video
    z-index: -1    the scrim
    z-index:  1    the header and the footer

  Negative z-index paints above the canvas background but below normal
  content, which is exactly the sandwich needed: the still shows through
  until the video covers it, and the scrim lands on whichever is visible.
*/
.backdrop {
  position: fixed;
  inset: 0;
  z-index: -2;

  width: 100%;
  height: 100%;
  /* Same behaviour as background-size: cover — fill the viewport, crop the
     overflow, never distort. */
  object-fit: cover;
  object-position: center center;

  pointer-events: none;
}

.home .hero::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;

  /* The light scrim: peaks at 0.30 and gone by 45% down. It keeps the white
     wordmark legible against the pale sky and the sunrise glare. */
  background: linear-gradient(
    180deg,
    rgba(10, 16, 26, 0.30) 0%,
    rgba(10, 16, 26, 0.16) 18%,
    rgba(10, 16, 26, 0.00) 45%
  );
}

/*
  The hero is exactly one screen, as it was before the footer existed. The
  footer follows it in normal flow, below the fold — so the first screen is
  unchanged and nothing new sits alongside the video.
*/
.home .hero {
  /* 100dvh, matching what body carried before the footer existed, so the
     hero is the same height it has always been. svh would be shorter on a
     phone and would let the footer peek above the fold. */
  min-height: 100vh;
  min-height: 100dvh;
}


/* ------------------------------------------------------------------ *
   Skip link
 * ------------------------------------------------------------------ */

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 10;
  padding: 12px 18px;
  background: var(--navy);
  color: var(--on-navy);
  font-size: 0.95rem;
  text-decoration: none;
}

.skip-link:focus {
  left: 0;
}


/* ------------------------------------------------------------------ *
   Header: wordmark left, hamburger right
 * ------------------------------------------------------------------ */

.site-header {
  /*
    Nav theme, interior pages: ink on paper. .home overrides every one of
    these below, so the hero keeps the exact treatment it already had.
  */
  --nav-ink: var(--ink);
  --nav-hover: var(--navy-tint);
  --nav-press: var(--navy-tint-strong);
  --nav-bar-shadow: none;
  --nav-panel-bg: var(--paper-raised);
  --nav-panel-border: var(--rule);
  --nav-panel-shadow: 0 18px 44px rgba(21, 24, 29, 0.12);
  --nav-item-ink: var(--ink);
  --nav-item-hover: var(--navy-tint);
  --nav-rule: var(--navy);
  /* Focus rings take the accent on paper, and turn white over the video. */
  --nav-focus: var(--navy);
  --wordmark-shadow: none;

  /* Above the video and the scrim. Also gives the dropdown a stacking
     context that cannot fall behind the background. */
  position: relative;
  z-index: 1;

  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--edge);
  padding: var(--edge);
}

/* Interior pages get a hairline under the nav; the hero must not. */
body:not(.home) .site-header {
  border-bottom: 1px solid var(--rule);
  padding-top: clamp(16px, 2.4vw, 26px);
  padding-bottom: clamp(16px, 2.4vw, 26px);
}

.home .site-header {
  --nav-ink: #fff;
  --nav-hover: rgba(255, 255, 255, 0.14);
  --nav-press: rgba(255, 255, 255, 0.20);
  --nav-bar-shadow: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.35));
  --nav-panel-bg: rgba(13, 22, 38, 0.72);
  --nav-panel-border: rgba(255, 255, 255, 0.14);
  --nav-panel-shadow: 0 18px 44px rgba(0, 0, 0, 0.38);
  --nav-item-ink: #fff;
  --nav-item-hover: rgba(255, 255, 255, 0.10);
  --nav-rule: #fff;
  --nav-focus: #fff;
  --wordmark-shadow: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.35));
  --wordmark-width: clamp(285px, 36vw, 540px);
}

.wordmark {
  display: block;
  width: var(--wordmark-width);
  height: auto;

  /*
    drop-shadow rather than box-shadow: it follows the letterforms through
    the PNG's alpha channel instead of drawing a rectangle around the file.
    Light — enough to hold an edge against bright sky, not enough to read as
    a shadow. None on paper, where there is nothing to hold an edge against.
  */
  filter: var(--wordmark-shadow);
}

.wordmark-link {
  display: block;
  border-radius: 4px;
}

.wordmark-link:focus-visible {
  outline: 2px solid var(--nav-focus);
  outline-offset: 6px;
}

/* Positioning context for the dropdown panel, which is anchored to it. */
.menu {
  position: relative;
  flex: none;
}


/* ------------------------------------------------------------------ *
   Hamburger button
 * ------------------------------------------------------------------ */

.menu-toggle {
  /* A <button> carries a pile of UA styling; strip it back to a bare box. */
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--nav-ink);
  cursor: pointer;

  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--bar-gap);

  /* 44px square: the bars are smaller, but the tappable area should not be. */
  width: 44px;
  height: 44px;
  border-radius: 8px;

  transition: background-color 0.2s ease;
}

.menu-toggle__bar {
  display: block;
  width: var(--bar-width);
  height: var(--bar-height);
  border-radius: 2px;
  background: currentColor;
  filter: var(--nav-bar-shadow);

  transition: transform 0.28s cubic-bezier(0.2, 0.7, 0.3, 1),
              opacity 0.18s ease,
              width 0.22s ease;
}

/* Hover: a soft pane lights up behind the bars and the middle one draws in. */
.menu-toggle:hover {
  background-color: var(--nav-hover);
}

.menu-toggle:hover .menu-toggle__bar:nth-child(2) {
  width: calc(var(--bar-width) * 0.65);
}

.menu-toggle:active {
  background-color: var(--nav-press);
}

/* Keyboard focus only — :focus-visible keeps the ring off mouse clicks. */
.menu-toggle:focus-visible {
  outline: 2px solid var(--nav-focus);
  outline-offset: 3px;
}

/*
  Open state: top and bottom bars converge on the centre line and cross; the
  middle one fades out. The translate distance is exactly the bar-to-bar
  spacing, so it is derived from the variables rather than hard-coded.
*/
.menu-toggle[aria-expanded="true"] .menu-toggle__bar:nth-child(1) {
  transform: translateY(calc(var(--bar-gap) + var(--bar-height))) rotate(45deg);
}

.menu-toggle[aria-expanded="true"] .menu-toggle__bar:nth-child(2) {
  opacity: 0;
  transform: scaleX(0.2);
}

.menu-toggle[aria-expanded="true"] .menu-toggle__bar:nth-child(3) {
  transform: translateY(calc(-1 * (var(--bar-gap) + var(--bar-height)))) rotate(-45deg);
}


/* ------------------------------------------------------------------ *
   Dropdown panel
 * ------------------------------------------------------------------ */

.menu-panel {
  position: absolute;
  top: calc(100% + 12px);
  right: 0;
  min-width: 210px;
  padding: 8px;

  border: 1px solid var(--nav-panel-border);
  border-radius: 12px;
  background: var(--nav-panel-bg);
  box-shadow: var(--nav-panel-shadow);

  /* Frosts whatever is behind the panel. Purely an enhancement; with the
     opaque panel on interior pages it has nothing to do. */
  -webkit-backdrop-filter: blur(12px) saturate(120%);
  backdrop-filter: blur(12px) saturate(120%);

  /*
    Collapsed by default. visibility (not just opacity) is what takes the
    links out of the tab order while shut, so a keyboard user cannot land on
    an invisible menu item. It is transitioned with a delay so it flips only
    after the fade finishes.
  */
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px) scale(0.98);
  transform-origin: top right;
  transition: opacity 0.22s ease,
              transform 0.22s ease,
              visibility 0s linear 0.22s;
}

.menu-panel.is-open {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity 0.22s ease,
              transform 0.22s ease,
              visibility 0s;
}

.menu-panel ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

.menu-panel li + li {
  margin-top: 2px;
}

.menu-item {
  position: relative;
  display: block;
  padding: 11px 14px 11px 18px;
  border-radius: 8px;

  color: var(--nav-item-ink);
  font-family: var(--font-sans);
  font-size: 1.05rem;
  font-weight: 500;
  line-height: 1.2;
  text-decoration: none;

  /*
    Uppercase via CSS rather than typing the labels in caps, so the markup
    still reads "Product" and a screen reader says "Product" rather than
    spelling out an acronym.

    Tracking flips sign for caps: capitals have no descenders or varied
    heights to separate them and set cramped at the negative value lowercase
    wanted.
  */
  text-transform: uppercase;
  letter-spacing: 0.07em;

  transition: background-color 0.18s ease,
              transform 0.18s ease;
}

/* A thin rule that grows out of the left edge on hover. */
.menu-item::before {
  content: "";
  position: absolute;
  left: 7px;
  top: 50%;
  width: 2px;
  height: 0;
  border-radius: 1px;
  background: var(--nav-rule);
  transform: translateY(-50%);
  transition: height 0.18s ease;
}

.menu-item:hover {
  background-color: var(--nav-item-hover);
}

.menu-item:hover::before {
  height: 56%;
}

.menu-item:focus-visible {
  outline: 2px solid var(--nav-focus);
  outline-offset: -2px;
}

/* The current page, marked with aria-current in the markup so the styling
   and the accessibility tree cannot disagree. */
.menu-item[aria-current="page"] {
  background-color: var(--nav-item-hover);
}

.menu-item[aria-current="page"]::before {
  height: 56%;
}


/* ------------------------------------------------------------------ *
   Page shell and prose
 * ------------------------------------------------------------------ */

.page {
  padding: clamp(56px, 11vh, 128px) var(--edge) clamp(72px, 14vh, 160px);
}

.wrap {
  max-width: var(--measure-wide);
  margin: 0 auto;
}

.page-head {
  max-width: var(--measure-wide);
  margin: 0 auto clamp(56px, 9vh, 104px);
}

.eyebrow {
  margin: 0 0 clamp(20px, 3vh, 34px);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--navy);
}

h1 {
  margin: 0;
  max-width: 18ch;
  font-size: clamp(2.6rem, 7vw, 4.6rem);
  font-weight: 600;
  line-height: 1.02;
  letter-spacing: -0.028em;
}

.lede {
  margin: clamp(24px, 4vh, 38px) 0 0;
  max-width: var(--measure);
  font-size: clamp(1.1rem, 1.6vw, 1.3rem);
  font-weight: 400;
  line-height: 1.5;
  color: var(--ink-soft);
}

.section {
  max-width: var(--measure-wide);
  margin: 0 auto;
  padding: clamp(40px, 7vh, 76px) 0;
  border-top: 1px solid var(--rule);
}

.section > p {
  max-width: var(--measure);
  margin: 0;
  font-size: 1.0625rem;
  line-height: 1.62;
}

.section > p + p {
  margin-top: 1.1em;
}

/* Small tracked label, the same treatment as the nav, for sub-sections. */
.section-label {
  margin: 0 0 clamp(14px, 2vh, 20px);
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--navy);
}

.section-heading {
  margin: 0 0 clamp(28px, 4.5vh, 48px);
  max-width: 22ch;
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.015em;
}

/*
  The line that carries the page. Given its own band between hairlines and
  nothing else on it — the emphasis is position and space, not decoration.
*/
.statement {
  max-width: var(--measure-wide);
  margin: 0 auto;
  padding: clamp(44px, 8vh, 88px) 0;
  border-top: 1px solid var(--rule);
  font-size: clamp(1.7rem, 4.4vw, 2.9rem);
  font-weight: 500;
  line-height: 1.12;
  letter-spacing: -0.022em;
}


/* ------------------------------------------------------------------ *
   Screenshots
 * ------------------------------------------------------------------ */

/*
  A screenshot is framed rather than dropped onto the page: white mount,
  hairline, and a shadow thrown far enough down to read as depth rather than
  as a border. The application is white; without the mount the shot would
  bleed into the page.
*/
.shot {
  margin: 0 auto;
  max-width: var(--measure-wide);
  padding: 14px;
  border: 1px solid var(--rule);
  border-radius: 18px;
  background: var(--paper-raised);
  box-shadow: 0 1px 1px rgba(22, 38, 74, 0.04),
              0 26px 50px -30px rgba(22, 38, 74, 0.45);
}

.shot img {
  display: block;
  border-radius: 10px;
}

.shot-hero {
  margin-bottom: clamp(8px, 2vh, 20px);
}

.shot-hero img {
  width: 100%;
  height: auto;
}

/*
  The three supporting shots. Two are landscape and one is portrait, so a
  plain row would step up and down. Each frame is therefore a fixed height
  and the image is contained inside it: every pixel of every screen stays
  visible, the portrait one simply sits smaller in its frame. Cropping to a
  common ratio was the alternative and it would have cut the buttons off the
  bottom of the two dialogs.
*/
.shots {
  display: grid;
  grid-template-columns: 1fr;
  /* Stacked one per row, so the gap is vertical air between whole cards
     rather than the gutter of a grid. */
  gap: clamp(52px, 8vh, 92px);
  max-width: var(--measure-wide);
  margin: 0;
  padding: 0;
  list-style: none;
}


/*
  No mount on these three. Each capture already contains its own frame — the
  dialogs bring the dark overlay border with them, the deal record its own
  edge — so wrapping them in a second bordered card drew two borders around
  one screenshot. They sit directly on the page and their own frames do the
  work. The hero above keeps its mount: a full-window capture has no edge of
  its own and would otherwise bleed into the page.

  The box is still a fixed height so the three captions line up, but it is
  taller than before and the padding is gone, which is most of what makes
  them bigger. Nothing is cropped: the portrait dialog fills the height, the
  two landscape ones fill the width, and the leftover space is page rather
  than an empty white card, so it does not read as a gap.
*/
/*
  The mount is back on all four. It came off when the two dialog captures
  carried their own dark surround and a second frame doubled it; those files
  have been replaced with transparent-cornered versions, so nothing in the
  pixels draws an edge any more and the page has to supply one.

  Every mount is the same height and the full width of its column, and the
  capture is contained inside it. Uniform frames are what keep the four
  captions on one line: each row here pairs a landscape capture with a
  portrait one, and letting each mount hug its own image would drop the
  captions to four different heights.

  Nothing is a fixed pixel width. The mounts are as wide as the column, so
  they track the container at every size, and the height is a clamp rather
  than a constant.
*/
/*
  One capture per row, each at its own size. No shared height any more: with
  the cards stacked there is no neighbour to line a caption up with, so each
  mount is free to be exactly as large as its image needs.

  The caps below are per capture because the four are different shapes. They
  are maxima, not widths — every mount is width: 100% underneath and simply
  stops growing at its cap, so the whole stack still reflows down to a phone.
*/
.shot-card .shot {
  display: block;
  width: 100%;

  padding: 14px;
  border: 1px solid var(--rule);
  border-radius: 14px;
  background: var(--paper-raised);
  box-shadow: 0 1px 1px rgba(22, 38, 74, 0.04),
              0 18px 36px -24px rgba(22, 38, 74, 0.40);
}

.shot-card .shot img {
  width: 100%;
  height: auto;
  /*
    The two dialogs were saved with transparent corners, flattened onto the
    same white as the mount, so their corners disappear into it and a radius
    here would only cut the frame short. The other two are full-bleed
    captures whose own corners are already square.
  */
  border-radius: 0;
}

/*
  Two captures side by side in one card.

  The split is 0.902 : 1, which puts the stages capture back at the 416px it
  was when it owned a whole row and gives the pipeline dialog whatever is
  left. Ratios rather than pixels, so the pair still tracks the container.

  align-items: center, not the grid default. The two are very different
  heights — 624px against 295px — and stretching would leave the shorter
  mount as a tall white box with a small dialog floating inside it. Each
  mount hugs its own image and the pair is centred on a common axis.
*/
.shot-pair {
  display: grid;
  grid-template-columns: 1fr;
  align-items: center;
  gap: clamp(16px, 2vw, 24px);
}

@media (min-width: 720px) {
  .shot-pair {
    grid-template-columns: 0.902fr 1fr;
  }
}

.shot-pair .shot {
  /* The per-capture caps below are for a capture that owns its whole row. */
  max-width: none;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* 656px image + 28px of mount padding. */
.shot-deal .shot,
.shot-newpipeline .shot {
  max-width: 684px;
}

/* 624px tall at each portrait's own ratio: 458px and 416px wide. */
.shot-newdeal .shot {
  max-width: 486px;
}

.shot-stages .shot {
  max-width: 444px;
}

.shot-title {
  margin: clamp(18px, 2.4vw, 24px) 0 0;
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--navy);
}

.shot-card p {
  margin: 10px 0 0;
  max-width: var(--measure);
  font-size: 1rem;
  line-height: 1.55;
  color: var(--ink);
}


/* ------------------------------------------------------------------ *
   The order, hub and spoke
 * ------------------------------------------------------------------ */

/*
  Mobile first, and the two layouts are genuinely different drawings:

    below 700px   a two-column stack, the centre card spanning the top and
                  the lines hidden. A radial diagram at phone width has its
                  labels colliding long before the geometry does.
    700px and up  eight lines converging on the centre, nodes placed by
                  coordinate.

  Thistle sets every node position with an inline style attribute. The CSP
  here forbids one outright, so every coordinate lives in this file and the
  markup carries nothing but a class.
*/
.hub-figure {
  position: relative;
  max-width: var(--measure-wide);
  margin: 0 auto;
}

.hub-lines {
  display: none;
}

.hub {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.hub li {
  display: flex;
  align-items: center;
  padding: 13px 18px;
  border: 1px solid var(--rule);
  border-radius: 14px;
  background: var(--paper-raised);
  color: var(--navy);
  font-size: 0.98rem;
  font-weight: 600;
  box-shadow: 0 1px 1px rgba(22, 38, 74, 0.04),
              0 12px 26px -18px rgba(22, 38, 74, 0.35);
}

.hub .hub-centre {
  grid-column: 1 / -1;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 22px 20px;
  border-radius: 18px;
  border-color: rgba(22, 38, 74, 0.22);
  box-shadow: 0 18px 44px -18px rgba(22, 38, 74, 0.40);
}

.hub-core-title {
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--navy);
}

.hub-core-sub {
  margin-top: 5px;
  font-size: 0.85rem;
  font-weight: 400;
  line-height: 1.4;
  color: var(--ink-soft);
}

@media (min-width: 700px) {
  .hub-figure {
    height: clamp(430px, 50vw, 530px);
  }

  /*
    No viewBox on purpose: without one the SVG user unit is the CSS pixel, so
    stroke-width is a true hairline at any container size and the percentage
    endpoints resolve against the element's own box. overflow: visible keeps
    a line from being clipped at the very edge of the box.
  */
  .hub-lines {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
  }

  .hub-lines line {
    stroke: var(--rule);
    stroke-width: 1.5;
  }

  /* The list stops being a grid and becomes a coordinate space. */
  .hub {
    display: block;
    position: absolute;
    inset: 0;
  }

  .hub li {
    position: absolute;
    /* Placed by its centre, so the line ends at the label rather than at its
       top-left corner. */
    transform: translate(-50%, -50%);
    justify-content: center;
    white-space: nowrap;
  }

  .hub .hub-centre {
    width: 230px;
  }

  .hub-companies  { left: 14%; top: 16%; }
  .hub-contacts   { left: 38%; top: 16%; }
  .hub-deals      { left: 62%; top: 16%; }
  .hub-quotes     { left: 86%; top: 16%; }
  .hub-centre     { left: 50%; top: 50%; }
  .hub-products   { left: 14%; top: 84%; }
  .hub-production { left: 38%; top: 84%; }
  .hub-shipping   { left: 62%; top: 84%; }
  .hub-invoicing  { left: 86%; top: 84%; }
}


/* ------------------------------------------------------------------ *
   Reveal on scroll
 * ------------------------------------------------------------------ */

/*
  Everything here is gated on `.js`, which reveal.js puts on <html> before
  the first paint. With JavaScript off the class never lands, none of these
  rules match, and every screenshot and every node is simply visible. Content
  is never hidden by default — that is the whole point of the gate.
*/
.js .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.6s cubic-bezier(0.2, 0.7, 0.3, 1),
              transform 0.6s cubic-bezier(0.2, 0.7, 0.3, 1);
}

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

/*
  No stagger. Stacked, each card crosses into view on its own, so a delay
  would read as the card being slow rather than as a sequence.
*/

/*
  The diagram is the exception. It is marked .reveal so the observer watches
  it, but the block itself does not move — its children do, which is what
  makes the spokes look like they are being drawn onto the page.
*/
.js .hub-figure.reveal {
  opacity: 1;
  transform: none;
}

.js .hub li {
  opacity: 0;
  transition: opacity 0.55s ease;
}

.js .hub-figure.is-in li {
  opacity: 1;
}

/* Centre first, then the spokes outward. The order of these matches the
   markup: centre, then the eight in reading order. */
.js .hub li:nth-child(1) { transition-delay: 0.10s; }
.js .hub li:nth-child(2) { transition-delay: 0.28s; }
.js .hub li:nth-child(3) { transition-delay: 0.34s; }
.js .hub li:nth-child(4) { transition-delay: 0.40s; }
.js .hub li:nth-child(5) { transition-delay: 0.46s; }
.js .hub li:nth-child(6) { transition-delay: 0.30s; }
.js .hub li:nth-child(7) { transition-delay: 0.36s; }
.js .hub li:nth-child(8) { transition-delay: 0.42s; }
.js .hub li:nth-child(9) { transition-delay: 0.48s; }

/*
  pathLength="1" in the markup normalises every line to a single unit
  regardless of its real length, so one dash offset draws all eight at the
  same rate no matter how far each has to travel.
*/
.js .hub-lines line {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  transition: stroke-dashoffset 0.9s cubic-bezier(0.2, 0.7, 0.3, 1);
}

.js .hub-figure.is-in .hub-lines line {
  stroke-dashoffset: 0;
}

.js .hub-lines line:nth-child(1) { transition-delay: 0.06s; }
.js .hub-lines line:nth-child(2) { transition-delay: 0.12s; }
.js .hub-lines line:nth-child(3) { transition-delay: 0.18s; }
.js .hub-lines line:nth-child(4) { transition-delay: 0.24s; }
.js .hub-lines line:nth-child(5) { transition-delay: 0.09s; }
.js .hub-lines line:nth-child(6) { transition-delay: 0.15s; }
.js .hub-lines line:nth-child(7) { transition-delay: 0.21s; }
.js .hub-lines line:nth-child(8) { transition-delay: 0.27s; }


/* ------------------------------------------------------------------ *
   Body links
 * ------------------------------------------------------------------ */

/*
  The first links in running text on this site. Underlined by a border
  rather than text-decoration so the rule sits clear of the descenders, and
  in the accent rather than ink so a link is distinguishable from bold text
  without relying on colour alone — the underline carries it either way.
*/
.text-link {
  color: var(--navy);
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px solid rgba(22, 38, 74, 0.35);
  padding-bottom: 2px;
  transition: border-color 0.18s ease;
}

.text-link:hover {
  border-bottom-color: var(--navy);
}

.text-link:focus-visible {
  outline: 2px solid var(--navy);
  outline-offset: 3px;
}

/* No destination yet. An <a> without an href is the spec's own placeholder
   link; the text cursor over it would be a smaller lie than a pointer. */
.text-link:not([href]) {
  cursor: default;
}

.closing-cta {
  margin: clamp(22px, 3vh, 32px) 0 0;
}


/* ------------------------------------------------------------------ *
   Leadership
 * ------------------------------------------------------------------ */

/*
  Square headshot, name, title, one line of proximity to the work. No bio, no
  profile link, no social icons — there is nothing else to say yet and filling
  it out is what would make it read like every other software company.

  Capped narrower than the content column: at full width two 480px portraits
  dominate a page whose subject is two people and six words.
*/
.leaders {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(36px, 5vw, 64px);
  max-width: 46rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 620px) {
  .leaders {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.leader img {
  display: block;
  width: 100%;
  height: auto;
  /* The sources are already 1:1; this holds the square if one ever is not. */
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border: 1px solid var(--rule);
}

.leader-name {
  margin: clamp(18px, 2.6vw, 26px) 0 0;
  font-size: clamp(1.15rem, 2vw, 1.35rem);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.leader-title {
  margin: 6px 0 0;
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--navy);
}

.leader-note {
  margin: clamp(12px, 1.8vw, 18px) 0 0;
  font-size: 1.0625rem;
  line-height: 1.55;
}


/* ------------------------------------------------------------------ *
   Footer
 * ------------------------------------------------------------------ */

.site-footer {
  position: relative;
  z-index: 1;
  background: var(--navy);
  color: var(--on-navy);
  padding: clamp(40px, 7vh, 72px) var(--edge);
}

.site-footer__inner {
  max-width: var(--measure-wide);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: clamp(24px, 4vw, 48px);
}

.site-footer__brand {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.site-footer__name {
  margin: 0;
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

/*
  The legal entity, set quieter than everything around it. No year: a static
  site has nobody to update it every January, and a stale "© 2026" reads worse
  than no year at all. No "All rights reserved" either — it carries no legal
  weight under Berne and this page does not spend words it does not need.
*/
.site-footer__legal {
  margin: 0;
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.62);
}

.site-footer__nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(14px, 2.4vw, 32px);
  margin: 0;
  padding: 0;
  list-style: none;
}

.footer-link {
  color: var(--on-navy);
  font-size: 0.82rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  padding-bottom: 3px;
  border-bottom: 1px solid transparent;
  transition: border-color 0.18s ease;
}

.footer-link:hover {
  border-bottom-color: var(--on-navy);
}

.footer-link:focus-visible {
  outline: 2px solid var(--on-navy);
  outline-offset: 4px;
}

.footer-link[aria-current="page"] {
  border-bottom-color: rgba(255, 255, 255, 0.45);
}


/* ------------------------------------------------------------------ *
   Phones
 * ------------------------------------------------------------------ */

/*
  Home only: the wordmark is three-quarters of the width of the screen.

  The header keeps zero horizontal padding and the insets are margins on the
  children instead. Percentage widths resolve against the containing block,
  so with padding on the header "75%" would mean 75% of what is left after
  the padding, not 75% of the screen.
*/
@media (max-width: 640px) {
  .home .site-header {
    padding: var(--edge) 0 0;
  }

  .home .wordmark {
    /* 75% of the unpadded header, i.e. of the viewport. Not 75vw, which on a
       desktop window narrowed below the breakpoint would also count the
       scrollbar gutter. */
    width: 75%;
    margin-left: var(--edge);
  }

  .home .menu {
    margin-right: var(--edge);
  }

  .site-footer__inner {
    flex-direction: column;
    align-items: flex-start;
  }
}


/* ------------------------------------------------------------------ *
   Reduced motion
 * ------------------------------------------------------------------ */

/*
  Honour the OS setting. The menu still opens, closes and morphs — it just
  arrives rather than animating. Not zero, because a 0s transition on
  visibility would let the panel keep focus for a frame.
*/
@media (prefers-reduced-motion: reduce) {
  .menu-toggle,
  .menu-toggle__bar,
  .menu-panel,
  .menu-item,
  .menu-item::before,
  .footer-link {
    transition-duration: 0.01ms;
  }

  /*
    A Ken Burns move is precisely what this setting exists to switch off:
    slow continuous drift is a common migraine and vestibular trigger. The
    video is removed and backdrop.jpg — the same picture, held still — shows
    through underneath. Nothing is lost but the movement.
  */
  .backdrop {
    display: none;
  }

  /*
    Reveal-on-scroll is motion the visitor did not ask for. Everything lands
    in place instead: no fade, no travel, no drawn lines.
  */
  .js .reveal,
  .js .hub li {
    opacity: 1;
    transform: none;
    transition: none;
    transition-delay: 0s;
  }

  .js .hub-lines line {
    stroke-dashoffset: 0;
    transition: none;
  }
}


/* ------------------------------------------------------------------ *
   Reduced data
 * ------------------------------------------------------------------ */

/*
  Honoured by Chrome and Edge when the visitor turns on Data Saver. A mill
  owner opening this on rural mobile should not spend 2.3 MB on decoration
  they did not ask for; they get the 293 KB still instead.
*/
@media (prefers-reduced-data: reduce) {
  .backdrop {
    display: none;
  }
}
