/* ==========================================================================
   base.css — fonts, tokens, reset, type primitives.

   The whole design system lives in this file. Two mechanisms generate almost
   every number on the site, both lifted as METHOD (not as pixels) from the
   benchmark study in docs/detail-floor.md:

   1. FLUID TYPE WITHOUT clamp(). Every type step declares two unitless
      numbers — --base-size (px at a 1440 reference viewport) and --min-size
      (a hard floor) — and shares one max() formula. Type scales linearly with
      the viewport until it hits its floor. There is no @media font override
      anywhere on this site.

   2. FLUID SPACE WITH NON-PROPORTIONAL FLOORS. Detail spacing (2/5/10/15/20)
      is frozen at every viewport; band spacing (30…120) compresses 17-33%.
      Components therefore look identical at every width and only the air
      between bands changes.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. FONTS — self-hosted. The page makes zero third-party requests.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: "Geist";
  src: url("../fonts/geist-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Geist";
  src: url("../fonts/geist-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Geist Mono";
  src: url("../fonts/geist-mono-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Geist Mono";
  src: url("../fonts/geist-mono-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Newsreader";
  src: url("../fonts/newsreader-latin-normal.woff2") format("woff2");
  font-weight: 200 800; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Newsreader";
  src: url("../fonts/newsreader-latin-italic.woff2") format("woff2");
  font-weight: 200 800; font-style: italic; font-display: swap;
}

/* --------------------------------------------------------------------------
   2. TOKENS
   -------------------------------------------------------------------------- */
:root {
  /* ---- the two engines -------------------------------------------------- */
  --ref: 1440;

  --space-3xs: 2px;
  --space-2xs: 5px;
  --space-xs:  10px;
  --space-sm:  15px;
  --space-md:  20px;
  --space-lg:  max(25px, calc((30  / var(--ref)) * 100vw));
  --space-xl:  max(30px, calc((40  / var(--ref)) * 100vw));
  --space-2xl: max(40px, calc((60  / var(--ref)) * 100vw));
  --space-3xl: max(60px, calc((80  / var(--ref)) * 100vw));
  --space-4xl: max(80px, calc((120 / var(--ref)) * 100vw));

  /* ---- colour ----------------------------------------------------------
     COLOUR LAW: no surface carries hue. Every neutral here is literally
     R = G = B. All chroma on this site is photographic and ships unwashed:
     no overlay, no opacity, no filter over any image, ever.
     These lightness values are matched to the benchmark's measured chrome;
     only its ~4-point cool cast has been removed.                          */
  --ground:      #FFFFFF;
  /* The one non-white surface left. It is no longer a chip fill — it is the
     well UNDER a piece of media, so a frame is a frame before its image or
     its poster has decoded, and nothing flashes white-on-white. Every use of
     it on this site is covered by a photograph a moment later. */
  --ground-2:    #EDEDED;   /* media wells only */
  --ink:         #000000;
  --ink-muted:   #6B6B6B;   /* benchmark measures #686C6D — hue removed */
  --border:      #DBDBDB;
  --neutral:     #A6A6A6;
  --on-ink:      #FFFFFF;

  /* ---- type ------------------------------------------------------------- */
  --sans:  "Geist", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
  --mono:  "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --serif: "Newsreader", "Iowan Old Style", Georgia, ui-serif, serif;

  --base-size: 14;
  --min-size: 13;

  /* The display step is the one size that is NOT on the fluid engine, because
     a display word has to span the MEASURE rather than hit a floor. It used
     to be a hand-fitted 14.6vw, which only fitted at one viewport and only
     fitted one word.

     It is now solved instead of fitted. --display-fit is the INK width of the
     display word in em, measured in the shipped face at the shipped tracking
     (Geist 500, -.05em): "egc" runs from the e's left ink edge to the c's
     right ink edge in 1.553em. Divide the measure by that and the word's ink
     spans the page inset exactly, at every viewport, with no media query.

     Fitting on INK rather than on the advance width matters here: the advance
     box is 1.576em, so fitting to it would leave the c's ink 17px past the
     right page margin at 1440. The matching optical pull on the left lives on
     .stage__word as a negative margin (B7).

     Change the word and this is the one number to re-measure. */
  --display-fit: 1.553;
  /* How much of the measure the mark spans. At 1 the word's ink lands on both
     page margins, which made it 890px tall at 1440 and a screen and a half of
     letterform before anything else could be read. Two thirds keeps it the
     largest object on the page by a wide margin and stops it being the only
     one. */
  --display-span: 0.66;
  --display-size: calc((100vw - var(--page-margin) * 2) * var(--display-span) / var(--display-fit));

  /* ---- geometry --------------------------------------------------------
     ONE radius. 100vw is reserved for shapes that are genuinely circular.  */
  --radius: 3px;
  --radius-round: 100vw;

  /* the page inset. The nav uses this same token, so the chips pin to the
     exact same line as the body copy — by construction, not coincidence. */
  --page-margin: clamp(20px, 2vw, 60px);
  --grid-gap: var(--space-md);

  /* The one fixed-width layout object on the site: a service card in the
     drifting band. Built on the same non-proportional max() the space scale
     uses, and fitted so four cards plus four gutters equal the measure at the
     reference viewport (4 x 325 + 4 x 20 = 1380, against a 1382px measure) — i.e. the drifting row and the resting four-across
     row are the same row. */
  --card: max(260px, calc((325 / var(--ref)) * 100vw));

  /* The two exceptions to the colour law, and the only chroma on this site
     that is not inside a photograph. They exist for the Merchant band's move
     column, where a delta's DIRECTION is the thing a reader resolves before
     they read the number. Both are dark: they read as ink that happens to be
     coloured, not as a highlight. Nothing else may use them. */
  --up:   #1B6B3A;
  --down: #A32B1E;

  --header-element-height: 40px;
  --header-padding-block: 15px;
  --header-height: calc(var(--header-element-height) + var(--header-padding-block) * 2);

  /* ---- motion ----------------------------------------------------------
     One easing. Three durations, and the durations carry meaning:
     .3s = you touched it.  .8s = something opened.  1.2s = it entered.    */
  --ease:    cubic-bezier(0.22, 1, 0.36, 1);
  --anim-sm: 0.3s var(--ease);
  --anim-md: 0.8s var(--ease);
  --anim-lg: 1.2s var(--ease);
}

@media (max-width: 768px) {
  :root { --header-element-height: 36px; --header-padding-block: 7px; }
}

/* --------------------------------------------------------------------------
   3. RESET
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/* Anything an in-page link can land on clears the fixed rail before it stops.
   Without this a jump puts the target's top hairline UNDER the nav and the
   section's label with it, so the reader arrives one band into the thing they
   asked for. The offset is the rail's own token plus one gutter, so it tracks
   the rail at every viewport instead of being a number someone measured once. */
section[id], main[id] { scroll-margin-top: calc(var(--header-height) + var(--space-md)); }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--fluid);
  line-height: 1.45;
  letter-spacing: 0;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: clip;
}

/* One weight on this site. Browsers default headings to bold; they don't here.
   Weight is chosen by the type step, never by the tag. */
h1, h2, h3, h4, h5, h6 { font-weight: 400; font-size: inherit; }
h1, h2, h3, h4, h5, h6, p, figure, blockquote { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
img, video { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; border-radius: var(--radius); }

a {
  color: inherit;
  text-decoration: none;
  transition: color var(--anim-sm), background-color var(--anim-sm),
              border-color var(--anim-sm), opacity var(--anim-sm);
}

:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: var(--radius); }
::selection { background: var(--ink); color: var(--ground); }

/* --------------------------------------------------------------------------
   4. TYPE SCALE
   Nine steps. Weight 400 throughout — there is no bold on this site. Line
   height tightens as size grows (1.2 → 1.1 → 1.0 → 0.9) and tracking goes
   monotonically negative with size, with one exception: the uppercase
   section label is LOOSENED to -.01em precisely because it is uppercase.
   -------------------------------------------------------------------------- */
/* A custom property that references var() is substituted where it is DECLARED,
   not where it is used — so --fluid must be declared on every element that
   consumes it, or each step silently inherits the root's size. Declaring it
   here, in the same rule that sets the two numbers, is what makes the engine
   work. This grouped selector is load-bearing; do not fold it into :root. */
.display, .h1, .h2, .h3, .h4, .h5, .h6, .body, .caption, .utility, .serif, body {
  --fluid: max(calc(var(--min-size) * 1px), calc((var(--base-size) / var(--ref)) * 100vw));
}

/* The display word is the wordmark, so it is set the way the mark is set:
   lowercase, 500, tight. The uppercase transform that used to live here was
   a property of the old word ("Commerce"), not of the step. */
.display {
  font-size: var(--display-size);
  line-height: 1;
  letter-spacing: -0.05em;
  font-weight: 500;
  text-transform: none;
}
.h1 { --base-size: 80; --min-size: 38; font-size: var(--fluid); line-height: 0.9; letter-spacing: -0.04em; }
.h2 { --base-size: 48; --min-size: 34; font-size: var(--fluid); line-height: 1;   letter-spacing: -0.04em; }
.h3 { --base-size: 40; --min-size: 30; font-size: var(--fluid); line-height: 1.1; letter-spacing: -0.04em; }
.h4 { --base-size: 32; --min-size: 26; font-size: var(--fluid); line-height: 1.1; letter-spacing: -0.04em; }
.h5 { --base-size: 24; --min-size: 20; font-size: var(--fluid); line-height: 1.1; letter-spacing: -0.01em;
      text-transform: uppercase; }
.h6 { --base-size: 18; --min-size: 16; font-size: var(--fluid); line-height: 1.2; letter-spacing: -0.02em; }

.body    { --base-size: 14; --min-size: 13; font-size: var(--fluid); line-height: 1.45; letter-spacing: 0; }
.caption { --base-size: 12; --min-size: 12; font-size: var(--fluid); line-height: 1.35; letter-spacing: 0; }

/* the one spice: mono, uppercase, the ONLY positive tracking on the site.
   Use it for figure numbers and plate data. Nothing else. */
.utility {
  --base-size: 12; --min-size: 10;
  font-size: var(--fluid);
  font-family: var(--mono);
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

/* the one earned serif: museum captions only. */
.serif {
  --base-size: 20; --min-size: 17;
  font-size: var(--fluid);
  font-family: var(--serif);
  font-style: italic;
  font-weight: 300;
  line-height: 1.4;
  letter-spacing: 0;
}

.muted { color: var(--ink-muted); }

/* Headings never carry a hand-made line break. Measure is set in em, so the
   character count per line is constant at every viewport. There is not one
   <br>, &nbsp; or wrapping <span> in any heading on this site. */
.display, .h1, .h2, .h3, .h4, .h5, .h6 { text-wrap: balance; }
.body, .caption, .serif { text-wrap: pretty; }

.m-14 { max-width: 14em; }
.m-16 { max-width: 16em; }
.m-20 { max-width: 20em; }
.m-24 { max-width: 24em; }
.m-40 { max-width: 40em; }

/* --------------------------------------------------------------------------
   5. LABELS — the flat law.

   There are no filled boxes on the page ground. A tag is black text on white,
   set apart by SPACE, not by a fill; a label/value pair is set apart by
   colour (the label quiet, the value ink); a badge is set apart by REGISTER
   (mono uppercase beside sentence-case sans). The grey chip was doing all
   three of those jobs with one rectangle.

   The only fill that survives is on media — see .reel__play in home.css —
   because a photograph has no white ground for black text to sit on.
   -------------------------------------------------------------------------- */
.tag {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4em;
  color: var(--ink);
  flex: 0 0 auto;
}
/* Inverted from the chip version on purpose: with the box gone, the QUIET
   half has to be the label, or a row of muted words reads as one weak
   sentence instead of a set of values. */
.tag .tag__value { color: var(--ink-muted); }
.tag .tag__text  { color: var(--ink); transition: color var(--anim-sm); }

/* The gap does the work the padding used to. It is the layout gutter, so a
   tag row shares its rhythm with every column on the site. */
.tags {
  display: flex; flex-direction: row; flex-wrap: wrap;
  gap: var(--space-2xs) var(--grid-gap);
}

/* A status badge. No box: mono uppercase at +.05em next to sentence-case sans
   is already a different register, which is the whole job the black chip was
   doing. */
.mark { color: var(--ink); }

/* The section count is GONE, and the component with it. A label used to carry
   a small raised number after it — RECENTS 02, IDEAS 03 — which is a count of
   the things below the label, said a second time in a second register. The
   list already says how many it holds by being a list. Round seven took every
   one of them off, sitewide, along with the leading ordinals on the service
   cards, the signal stages and the pipeline nodes; what survives is the three
   places a number is an ADDRESS rather than a tally: a figure plate, a row's
   rank in the re-rank table, and the entry numbers on the work stage, which
   are its only position control. See the round-seven note in
   docs/detail-floor.md. */

/* --------------------------------------------------------------------------
   6. LAYOUT
   Edge-to-edge with a token inset. There is no centred max-width container.
   -------------------------------------------------------------------------- */
.shell { padding-inline: var(--page-margin); }

/* Bands are symmetric by default — the pair is written out longhand so the
   next person to edit it can see that the symmetry is intentional. */
.band   { padding-block: var(--space-4xl) var(--space-4xl); }
.band-s { padding-block: var(--space-2xl) var(--space-2xl); }
/* the one systematic asymmetry: the first band clears the fixed nav with
   one nav-height of air beneath it. */
.band-first { padding-block: calc(var(--header-height) * 1.5) var(--space-xl); }
/* The RAIL opener's band, and only it. Its bottom padding is the entire gap
   between the STUDIO line and the stage below — the stage carries no top
   padding of its own (H, round four) — so this one number is the whole band
   of air under the opener. Two rounds of tightening have taken it from
   --space-4xl through --space-xl to --space-md: 120 → 40 → 20 at the
   reference viewport, and 20px is where a 12px caption row still has air
   under it at 390. Written longhand, and scoped to .band-rail rather than
   applied to .band-first, because every OTHER first band on the site opens
   on a statement and a display word and needs the larger number. */
.band-first.band-rail { padding-block: calc(var(--header-height) * 1.5) var(--space-md); }

.rule-top { border-top: 1px solid var(--border); }

/* --- the opener: label + count LEFT, statement RIGHT ---------------------
   1fr 2fr. The label is a heading step that happens to be uppercase, not a
   micro-label, which is what gives the opener its weight. The two columns
   align at box-top, NOT at baseline — the label's baseline riding a few px
   above the statement's is what makes it read as an annotation.            */
.opener {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: var(--grid-gap);
  align-items: start;
}
/* Baseline, so a 12px status mark beside a 24px label sits ON the label's
   line rather than hanging off its cap height. It is a no-op for the openers
   that carry a label and nothing else, which is most of them. */
.opener__label { display: flex; flex-direction: row; align-items: baseline; gap: var(--space-xs); }
.opener__body  { display: flex; flex-direction: column; gap: var(--space-sm); align-items: flex-start; }

/* --- the opener as a RAIL ------------------------------------------------
   One modifier, for the opener that carries a label and nothing opposite it
   but a set of registers. Two things change, and only two.

   ALIGNMENT. C3's box-top rule exists so a 24px label reads as an annotation
   over a 32px statement. There is no statement here: the thing opposite the
   label is a 12px caption row, and box-top alignment hangs it off the label's
   cap line with nine px of air under it — a dropped line rather than a
   matched pair. Baseline is what makes two sizes sit on ONE line, which is
   what this opener now is. C3 still governs every opener with a statement in
   it, which is every other opener on the site.

   SIDE. The registers are pushed to the right edge of the 2fr column, so they
   land on --page-margin: the same line the Connect link in the fixed rail
   above them lands on, from the same token. Label and registers pin the two
   page insets, and the row reads as a rail rather than as a left-weighted
   label with a list trailing after it.                                     */
.opener--rail { align-items: baseline; position: relative; z-index: 1; }
.tags--end { justify-content: flex-end; }

@media (max-width: 768px) {
  .opener { grid-template-columns: 1fr; gap: var(--space-md); }
  /* One column: there is no right edge to pin to and no second inset to rhyme
     with, so the row returns to the page's single left inset like everything
     else on a phone. */
  .tags--end { justify-content: flex-start; }
}

/* --- framed media -------------------------------------------------------
   Four display ratios exist. Nothing else is allowed. Cropping is always
   object-fit: cover; source dimensions live on the width/height attributes
   as CLS hints only.                                                       */
.r-34  { aspect-ratio: 3 / 4; }
.r-169 { aspect-ratio: 16 / 9; }
.r-43  { aspect-ratio: 4 / 3; }
/* 1:1 has been in the set since the floor was written (D4) and finally has a
   use: the finish tiles and the node-canvas thumbnails, where the subject is
   a pattern and a square is the honest frame for it. Not a new ratio. */
.r-11  { aspect-ratio: 1 / 1; }
/* The fifth and last ratio. Reserved for the ONE full-bleed capture band: a
   screen recording's proportion is set by the machine that made it, not by
   this scale, so it is declared here rather than cropped into a lie. Any
   second use of this class means the set has stopped being a set. */
.r-21  { aspect-ratio: 2 / 1; }

/* --- link ---------------------------------------------------------------
   No padding, no box, no negative margin to cancel the padding: the link
   starts exactly on the column edge because nothing is inset. Its
   affordance is a HAIRLINE under the text alone — the arrow is not
   underlined, because the arrow is the gesture, not the label.            */
.link {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  color: var(--ink);
}
.link__label {
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.2em;
  transition: border-color var(--anim-sm);
}
.link:hover .link__label { border-color: var(--ink); }
.link__arrow { transition: translate var(--anim-sm); }
.link:hover .link__arrow { translate: 3px 0; }

.sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
      overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* --------------------------------------------------------------------------
   7. VIEWPORT REVEAL
   The third duration finally means what the detail floor says it means:
   1.2s is "it entered the viewport". One gesture — 15px up, into opacity —
   declared per-element with data-reveal, driven by one IntersectionObserver.
   No JS, or reduced motion: everything is simply visible. The .is-in class is
   added once and never removed, so nothing re-animates on scroll-back.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  html.js [data-reveal] {
    opacity: 0;
    translate: 0 15px;
    transition: opacity var(--anim-lg), translate var(--anim-lg);
  }
  html.js [data-reveal].is-in { opacity: 1; translate: 0 0; }
}
