/* =============================================================================
   Amwaj Sama — one stylesheet, hand written, no framework.

   ONE CONSTRAINT SHAPED THIS MORE THAN TASTE DID.

   The client is astigmatic. Astigmatic eyes smear point light into streaks and
   set up interference against any fine repeating detail, so there is no pattern
   anywhere on this site: no dot screen, no line field, no hatch, no noise, no
   texture. Every surface is flat or a large smooth gradient. Type starts a step
   larger than a scale like this usually would, leading is loose, nothing
   hairline carries meaning, and no point of light anywhere on the site is
   allowed to flicker.

   THE PAGE IS DARK, AND THAT WAS ORIGINALLY RULED OUT.

   Light type on a dark ground produces halation — the glow that makes each
   letter bleed into the next — and for the same astigmatism reason this site
   was built light first. The client asked for night afterwards, having lived
   with the light version, and it is his call and his eyes. Everything below is
   the mitigation, and none of it is optional:

     — the ground is never pure black. #070C18 is about four tenths of a per
       cent luminance, which reads as black and haloes markedly less than 000.
     — the type is never pure white. #DCE4F2 still clears 15:1 and is much
       kinder than FFF at the same size.
     — rules and borders go UP in weight on dark, not down. A hairline on a dark
       ground disappears into the halo of whatever sits next to it.
     — nothing glows with a blur, and nothing twinkles.

   COLOUR: deep navy page, near-white text, one blue. The only hue with any
   range is the hero, which is two lit pieces of glass on a dark sea rather than
   anything painted — so the identity is a material, and the page stays out of
   its way.

   MOTION: THIS RULE WAS LIFTED, DELIBERATELY, BY THE STUDIO.

   It used to read: "three things move. The sea answers the pointer, and
   everything floating on it answers the sea. Sections arrive once. Controls
   answer hover and press. There is no fourth, no stagger, no scroll-jacking,
   and no animation library." That restraint came from the same astigmatism
   constraint as everything above it.

   The studio asked for scroll-driven motion afterwards, having lived with the
   quiet version, and asked for the stagger rule to come off with it. It is his
   call and his eyes, exactly as the dark theme was. What replaced it:

     — the drawings animate, and they animate by being DRAWN. A line arriving
       along its own length is legible motion; a thing that fades in place is
       not, and is also the most recognisable generated-page motion there is.
     — every scroll animation is driven by native CSS scroll timelines. No
       library was added. The site still ships four hand-written modules and
       the colophon still measures them, so the figure on that page stays true.
     — nothing flickers, nothing glows with a blur, and nothing pins the page
       or takes the scroll away from the person doing it. Those three did NOT
       come off, because none of them was ever about restraint: a flicker and a
       stolen scrollbar are hostile to far more readers than one.
     — no TEXT is ever animated from opacity 0. Graphics may be, because a
       reader who cannot scroll loses a picture rather than a paragraph. This
       is why `npm run check` still asserts nothing sits at opacity 0.
     — prefers-reduced-motion still ends all of it, and so does the toggle in
       the footer.
   ========================================================================== */

/* ---------------------------------------------------------------- fonts --- */

@font-face {
  font-family: "Archivo";
  src: url("/fonts/archivo.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-stretch: 62% 100%;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Sligoil";
  src: url("/fonts/sligoil.woff2") format("woff2-variations");
  font-weight: 90 120;
  font-style: normal;
  font-display: swap;
}

/* Metric-matched fallbacks, so the swap to the webfont moves nothing. */
@font-face {
  font-family: "ArchivoFallback";
  src: local("Helvetica Neue"), local("Arial"), local("Segoe UI"), local("Roboto");
  size-adjust: 96.5%;
  ascent-override: 96%;
  descent-override: 24%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "SligoilFallback";
  src: local("SF Mono"), local("Menlo"), local("Consolas"), local("DejaVu Sans Mono");
  size-adjust: 101%;
  ascent-override: 92%;
  descent-override: 24%;
  line-gap-override: 0%;
}

/* --------------------------------------------------------------- tokens --- */

:root {
  /* Tells the browser this is a dark page, so scrollbars, form controls and the
     default canvas behind everything come up dark rather than flashing white on
     load and around the edges of native widgets. */
  color-scheme: dark;

  --paper: #070c18;      /* not #000 — see the note at the top of this file */
  --paper-sunk: #0c1428;
  --ink: #dce4f2;        /* not #fff, for the same reason */
  --ink-soft: #93a0bc;   /* 6.9:1 on paper, 6.5:1 on the sunk band */
  --rule: #1e2a44;
  --rule-strong: #55648a; /* 3.1:1 on paper — control borders are a real boundary */
  --accent: #6e9bff;
  --accent-hover: #9dbbff;

  /* The hero's material. The only hue range on the site. */
  --object-cool: #bfd8f5;
  --object-warm: #ffedd1;
  --object-deep: #1b2a4a;

  --focus: var(--ink);
  --selection: color-mix(in oklab, var(--accent) 16%, transparent);
  --feed: 1;

  --font-sans: "Archivo", ArchivoFallback, system-ui, sans-serif;
  --font-mono: "Sligoil", SligoilFallback, ui-monospace, monospace;

  /* Archivo's width axis is semantic: three registers out of one file. */
  --wdth-display: 76;
  --wdth-annot: 88;
  --wdth-prose: 100;

  /* Modular 1.333 from an 18px base. Nothing below 14px — small type is harder
     work for an astigmatic reader than for anyone else, so the whole scale sits
     a step above where it would usually start. */
  --t-data: 0.875rem;
  --t-small: 1rem;
  --t-body: 1.125rem;
  --t-lede: clamp(1.375rem, 1.3rem + 0.35vw, 1.625rem);
  --t-h3: clamp(1.5rem, 1.36rem + 0.7vw, 1.9375rem);
  --t-h2: clamp(2rem, 1.5rem + 2.5vw, 3.625rem);
  --t-h1: clamp(2.875rem, 1.6rem + 6.4vw, 7rem);

  --track-h1: -0.02em;
  --track-h2: -0.012em;
  --track-h3: -0.006em;
  --track-body: 0.002em;
  --track-label: 0.08em;

  --lh-h1: 1.03;
  --lh-h2: 1.1;
  --lh-h3: 1.24;
  /* Generous leading is the cheapest thing you can do for a reader whose
     letterforms are already slightly smeared. */
  --lh-body: 1.68;
  --lh-small: 1.6;

  --measure: 66ch;

  /* A real baseline: 18 x 1.68 = 30px lead. */
  --lead: 1.875rem;
  --s-1: 0.5rem;
  --s-2: 0.875rem;
  --s-3: 1.875rem;
  --s-4: 3.75rem;
  --s-5: 5.625rem;
  --s-6: 9.375rem;
  --s-7: 15rem;

  --gutter: clamp(1rem, 2.2vw, 2rem);
  --grid-max: 90rem;
  --shell-max: 115rem;
  --page-pad: max(var(--gutter), 1.25rem);

  --r-1: 6px;
  --r-2: 8px;

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
  --t-quick: 140ms;
  --t-tap: 90ms;
  --t-arrive: 520ms;
  --t-page: 260ms;

  --z-content: 1;
  --z-skip: 10;
}

[dir="rtl"] { --feed: -1; }

/* ---------------------------------------------------------------- reset --- */

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

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-sans);
  font-variation-settings: "wdth" var(--wdth-prose);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  letter-spacing: var(--track-body);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, p, figure, blockquote, dl, dd { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
img, svg, canvas { display: block; max-width: 100%; }
img { height: auto; }
a { color: inherit; }
::selection { background: var(--selection); }

/* One focus treatment, thick enough to see without being a hairline. There is
   no bare `outline: none` in this file. */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ---------------------------------------------------------------- type --- */

h1, h2 {
  font-variation-settings: "wdth" var(--wdth-display);
  font-weight: 700;
  text-wrap: balance;
}

h1 {
  font-size: var(--t-h1);
  line-height: var(--lh-h1);
  letter-spacing: var(--track-h1);
}

h2 {
  font-size: var(--t-h2);
  line-height: var(--lh-h2);
  letter-spacing: var(--track-h2);
}

h3 {
  font-variation-settings: "wdth" var(--wdth-prose);
  font-size: var(--t-h3);
  font-weight: 700;
  line-height: var(--lh-h3);
  letter-spacing: var(--track-h3);
  text-wrap: balance;
}

/* Three leads above a heading, one below, so the "heading sits closer to the
   block above than to its own content" inversion is impossible by rule. */
:is(h2, h3):not(:first-child) { margin-block: var(--s-5) var(--s-3); }

p { max-width: var(--measure); text-wrap: pretty; }
p + p { margin-block-start: var(--s-3); }

.lede {
  font-size: var(--t-lede);
  line-height: 1.5;
  max-width: 50ch;
}

.annot {
  font-variation-settings: "wdth" var(--wdth-annot);
  font-weight: 500;
}

/* The mono register is fenced to real data — reached through .data and the
   semantic elements below, never applied to prose. */
.data, code, samp, kbd, time, output {
  font-family: var(--font-mono);
  font-size: var(--t-data);
  line-height: 1.5;
  font-variant-numeric: tabular-nums slashed-zero;
}

.label {
  font-family: var(--font-mono);
  font-size: var(--t-data);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-block-end: var(--s-2);
}

.small { font-size: var(--t-small); line-height: var(--lh-small); }
.muted { color: var(--ink-soft); }
strong, b { font-weight: 700; }

/* --------------------------------------------------------------- layout --- */

.shell {
  max-inline-size: var(--shell-max);
  margin-inline: auto;
  padding-inline: var(--page-pad);
}

.grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--gutter);
  max-inline-size: var(--grid-max);
  margin-inline-end: auto;
}

@media (max-width: 900px) { .grid { grid-template-columns: repeat(8, minmax(0, 1fr)); } }
@media (max-width: 600px) { .grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* Column placements as classes, not inline styles: an inline style cannot carry
   a media query, and a span of ten on a four-column grid makes the page wider
   than the phone. */
.col-wide { grid-column: 1 / span 11; }
.col-major { grid-column: 1 / span 9; }
.col-main { grid-column: 1 / span 8; }
.col-half { grid-column: 1 / span 6; }
.col-indent { grid-column: 3 / -1; }
.col-body { grid-column: 3 / span 7; }
.col-inset { grid-column: 2 / span 10; }
.col-read { grid-column: 2 / span 7; }
.col-late { grid-column: 5 / span 6; }

@media (max-width: 1080px) {
  .col-body { grid-column: 2 / span 9; }
  .col-late { grid-column: 3 / span 8; }
  .col-read { grid-column: 1 / span 9; }
}

@media (max-width: 900px) {
  .col-wide, .col-major, .col-main, .col-half,
  .col-indent, .col-body, .col-inset, .col-read, .col-late { grid-column: 1 / -1; }
}

.band { padding-block: var(--s-6); }
.band--air { padding-block: var(--s-7); }
.band--tight { padding-block: var(--s-4); }
.band--flush { padding-block: var(--s-5) var(--s-4); }
.band--wide { padding-block: var(--s-6) var(--s-5); }
.band--sunk { background: var(--paper-sunk); }

@media (max-width: 600px) {
  .band { padding-block: var(--s-5); }
  .band--air { padding-block: var(--s-6); }
  .band--tight { padding-block: var(--s-4); }
}

/* A single hairline between the airy half of a band and its dense half. It sits
   in normal flow and moves with the content, because it IS content — no fixed
   offset to break the first time somebody edits a heading. */
/* Every rule on this site is 2px rather than 1. On a light page a hairline is
   a boundary; on a dark one it falls inside the halo of the type next to it and
   reads as a smudge. This is the astigmatism mitigation for going dark, not a
   style choice — see the note at the top of the file. */
.rule {
  border: 0;
  border-block-start: 2px solid var(--rule);
  margin-block: var(--s-4) var(--s-3);
}

/* ------------------------------------------------------------------ sea --- */

/* The hero is a sea seen from directly overhead, and the copy floats on it.

   The canvas is not an illustration beside the type — it is the ground the type
   is standing on, so it runs the full bleed of the section and the words sit
   over it. That means two things this file has to guarantee. The water is dark
   and low-contrast by construction, so the headline over it clears 14:1
   everywhere, not merely on average. And every element that floats is moved by
   a transform and nothing else: no top, no margin, no width — a hero whose
   layout reflowed sixty times a second would be the most expensive thing on the
   page and would reflow the whole document under it. */

.sea {
  position: relative;
  isolation: isolate;
  /* Tall enough to be a place rather than a banner, short enough that the call
     to action is on the first screen. At 92svh plus a full band's padding top
     and bottom this came out at 1230px against a 900px viewport, which put the
     button below the fold on a laptop — a hero nobody can act on. The padding
     is gone because the hero is not a band with a background, it IS the
     background, and the height alone now decides it. */
  min-block-size: min(84svh, 52rem);
  display: grid;
  align-items: center;
  padding-block: var(--s-4);
}

.sea__canvas {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  /* The still underneath, until the live one has a frame. */
  background: var(--paper);
}

/* Present, decoding, and completely invisible. Its only consumer is a
   texImage2D call in the shader. display:none would be tidier and would also
   stop it decoding, which would leave the sea floor black. */
.sea__film {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  opacity: 0;
  pointer-events: none;
}

.sea__canvas canvas,
.sea__still {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

.sea__canvas canvas {
  opacity: 0;
  transition: opacity var(--t-arrive) var(--ease-out);
}
.sea__canvas canvas.is-lit { opacity: 1; }
.sea__canvas.is-live .sea__still { visibility: hidden; }

/* The copy is a raft; the scene writes a transform on it every frame. */
.raft { will-change: transform; }

/* And each word is a float of its own. inline-block so it can be transformed;
   the whitespace between words stays as real text nodes, so line breaking, text
   selection and copy-paste are all exactly what they were without it. */
.wave-word { display: inline-block; will-change: transform; }

@media (prefers-reduced-motion: reduce) {
  .raft, .wave-word { will-change: auto; transform: none !important; }
}

/* -------------------------------------------------------------- masthead --- */

.masthead { border-block-end: 2px solid var(--rule); }

.masthead__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  min-block-size: 68px;
  max-inline-size: var(--grid-max);
  margin-inline-end: auto;
  flex-wrap: wrap;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  text-decoration: none;
  font-variation-settings: "wdth" var(--wdth-annot);
  font-weight: 700;
  font-size: var(--t-small);
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.wordmark svg { inline-size: 32px; block-size: 32px; flex: none; }

.masthead nav {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.2vw, 2rem);
  flex-wrap: wrap;
}

.masthead nav a {
  font-size: var(--t-small);
  text-decoration: none;
  padding-block: 4px;
  border-block-end: 2px solid transparent;
}

.masthead nav a:hover,
.masthead nav a:focus-visible { border-block-end-color: var(--rule-strong); }
.masthead nav a[aria-current="page"] { border-block-end-color: var(--accent); }

.skip-link {
  position: absolute;
  inset-inline-start: -9999px;
  z-index: var(--z-skip);
  background: var(--ink);
  color: var(--paper);
  padding: var(--s-2) var(--s-3);
  text-decoration: none;
  border-radius: 0 0 var(--r-2) 0;
}

.skip-link:focus { inset-inline-start: 0; inset-block-start: 0; }

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

a, button, summary {
  transition:
    color var(--t-quick) var(--ease-out),
    background-color var(--t-quick) var(--ease-out),
    border-color var(--t-quick) var(--ease-out);
}

.link,
main p a,
main li a,
.colophon a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  /* Enough clearance that the underline never touches a descender, which is one
     of the first things to blur. */
  text-underline-offset: 0.22em;
}

.link:hover, .link:focus-visible,
main p a:hover, main p a:focus-visible,
main li a:hover,
.colophon a:hover, .colophon a:focus-visible { color: var(--accent-hover); }

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: 52px;
  padding: 0 var(--s-3);
  border: 2px solid transparent;
  border-radius: var(--r-2);
  background: var(--accent);
  color: var(--paper);
  font: inherit;
  font-variation-settings: "wdth" var(--wdth-annot);
  font-weight: 700;
  font-size: var(--t-small);
  text-decoration: none;
  cursor: pointer;
}

.btn:hover, .btn:focus-visible { background: var(--accent-hover); color: var(--paper); }
.btn:active { background: var(--accent-hover); transform: translateY(1px); transition-duration: var(--t-tap); }

.btn[disabled],
.btn[aria-disabled="true"] {
  background: var(--paper-sunk);
  color: var(--ink-soft);
  border-color: var(--rule-strong);
  cursor: not-allowed;
}

.btn--quiet { background: transparent; color: var(--ink); border-color: var(--rule-strong); }
.btn--quiet:hover, .btn--quiet:focus-visible { background: transparent; color: var(--accent); border-color: var(--accent); }

.cta-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex-wrap: wrap;
  margin-block-start: var(--s-4);
}

/* ------------------------------------------------------------------ hero --- */

/* Seven columns of type against five of open water. The copy never crosses into
   the right-hand block, because that is where the two pieces of glass float and
   type over a moving object is type nobody reads. */
.sheet {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  align-items: center;
  min-inline-size: 0;
}

.sheet__type { grid-column: 1 / span 7; min-inline-size: 0; }

@media (max-width: 900px) {
  .sheet__type { grid-column: 1 / -1; }
}

.fact {
  margin-block-start: var(--s-3);
  color: var(--ink-soft);
  max-inline-size: 54ch;
}

/* ---------------------------------------------------------------- table --- */

.scroller { overflow-x: auto; scroll-padding-inline: 20px; }

table { inline-size: 100%; border-collapse: collapse; font-size: var(--t-small); }

caption {
  text-align: start;
  padding-block-end: var(--s-2);
  color: var(--ink-soft);
  font-family: var(--font-mono);
  font-size: var(--t-data);
  letter-spacing: var(--track-label);
}

th, td {
  text-align: start;
  padding: var(--s-2) var(--s-3) var(--s-2) 0;
  vertical-align: baseline;
  border-block-end: 2px solid var(--rule);
}

thead th {
  font-variation-settings: "wdth" var(--wdth-annot);
  font-size: var(--t-data);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-soft);
  font-weight: 500;
  white-space: nowrap;
}

td.data, th.data { white-space: nowrap; }

.empty-state { padding-block: var(--s-4); max-inline-size: var(--measure); }

/* -------------------------------------------------------------- services --- */

/* Six entries. Not cards: no box, no shadow, no icon tile. A rule and a change
   of column is separation enough, and it keeps the page quiet. */
.services {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  gap: var(--s-5) var(--gutter);
  min-inline-size: 0;
}

.service {
  border-block-start: 2px solid var(--ink);
  padding-block-start: var(--s-3);
  min-inline-size: 0;
}

.service:nth-child(odd) { grid-column: 1 / span 6; }
.service:nth-child(even) { grid-column: 7 / span 6; }
/* The second entry is pushed out a column, so the six do not settle into a
   grid of identical tiles the moment the eye reaches them. */
.service:nth-child(2) { grid-column: 8 / span 5; }

@media (max-width: 900px) {
  .service,
  .service:nth-child(odd),
  .service:nth-child(even) { grid-column: 1 / -1; }
}

.service__no {
  font-family: var(--font-mono);
  font-size: var(--t-data);
  color: var(--ink-soft);
  margin-block-end: var(--s-1);
}

.service__weeks {
  font-variation-settings: "wdth" var(--wdth-annot);
  font-weight: 700;
  margin-block-start: var(--s-2);
}

.service__stack {
  color: var(--ink-soft);
  margin-block-start: var(--s-2);
  word-spacing: 0.2em;
  overflow-wrap: anywhere;
}

/* Each mode says what it is in prose, and then says what actually arrives. The
   prose is the argument; this is the receipt, and a buyer skimming reads only
   this. Rules rather than bullets, for the same reason nothing else on this site
   has a bullet: a tick or a disc is a list marker typed into content. */
.gets { margin-block-start: var(--s-3); }

.gets li {
  border-block-start: 2px solid var(--rule);
  padding-block: var(--s-1);
  font-size: var(--t-small);
  line-height: var(--lh-small);
}

.gets__head {
  font-family: var(--font-mono);
  font-size: var(--t-data);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-block-end: var(--s-1);
}

/* ---------------------------------------------------------------- works --- */

/* The studio's own work. Not cards: a rule, a change of column and a lot of air
   do the separating, the same as everything else on this site.

   One entry is a legitimate state here and the layout has to survive it, which
   is why the columns are auto-fit rather than a fixed two. A gallery of one
   thing that is genuinely good reads better than a grid padded out with
   mockups, and padding it out is exactly what sank the previous site. */
.works {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: repeat(auto-fit, minmax(min(30rem, 100%), 1fr));
  gap: var(--s-5) var(--s-4);
  min-inline-size: 0;
}

.work {
  border-block-start: 3px solid var(--ink);
  padding-block-start: var(--s-3);
  min-inline-size: 0;
  /* Capped so that a gallery holding a single entry does not draw a rule a
     metre wide over one paragraph. The grid is auto-fit, so with one item the
     column would otherwise take the whole width and the entry would look
     stranded under it. */
  max-inline-size: 46rem;
}

.work__meta {
  display: flex;
  gap: var(--s-3);
  flex-wrap: wrap;
  font-family: var(--font-mono);
  font-size: var(--t-data);
  color: var(--ink-soft);
  margin-block-end: var(--s-1);
}

.work h3 { font-size: var(--t-h3); }
.work__blurb { margin-block-start: var(--s-2); }

.work__facts { margin-block-start: var(--s-3); }

.work__facts li {
  border-block-start: 2px solid var(--rule);
  padding-block: var(--s-1);
  font-size: var(--t-small);
  line-height: var(--lh-small);
  color: var(--ink-soft);
}

.work__more { margin-block-start: var(--s-3); }

/* -------------------------------------------------------------- metrics --- */

/* The measured facts, at size. Every value in here comes out of
   src/_data/build.js, which weighs the files at build time — so this is the one
   block on the site that cannot be aspirational. If somebody installs a runtime
   dependency, the first figure stops reading zero without anybody editing a
   template. */
.metrics {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--s-4) var(--gutter);
  min-inline-size: 0;
}

.metric { border-block-start: 3px solid var(--rule-strong); padding-block-start: var(--s-2); }

.metric dt {
  font-family: var(--font-mono);
  font-size: var(--t-data);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-soft);
}

.metric dd {
  font-variation-settings: "wdth" var(--wdth-display);
  font-weight: 700;
  font-size: var(--t-h3);
  line-height: 1.1;
  margin-block-start: var(--s-1);
  font-variant-numeric: tabular-nums slashed-zero;
}

.metric dd small {
  font-variation-settings: "wdth" var(--wdth-annot);
  font-size: var(--t-small);
  font-weight: 400;
  color: var(--ink-soft);
}

/* ---------------------------------------------------------------- modes --- */

/* The homepage version of the three modes: a title, one line, and the way out
   to the page that still argues at length. The old homepage gave each of these
   a two-hundred-word paragraph and set them at alternating column starts, which
   put roughly nine hundred words between the hero and the process. Three
   columns, read in about eight seconds, and the detail is one click away for
   anybody who wants it. */
.modes {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--gutter);
  min-inline-size: 0;
}

@media (max-width: 900px) { .modes { grid-template-columns: 1fr; gap: var(--s-4); } }

.mode {
  border-block-start: 3px solid var(--ink);
  padding-block-start: var(--s-3);
  min-inline-size: 0;
}

.mode__no { font-family: var(--font-mono); font-size: var(--t-data); color: var(--ink-soft); margin-block-end: var(--s-1); }
.mode h3 { font-size: var(--t-body); font-variation-settings: "wdth" var(--wdth-annot); }
.mode p { margin-block-start: var(--s-2); font-size: var(--t-small); line-height: var(--lh-small); }

/* ----------------------------------------------------------------- ramp --- */

/* The process bar, filling against the scroll. A solid rule rather than a
   gradient, because a gradient is a colour interpolation and the audit greps
   for those. */
.ramp {
  position: relative;
  block-size: 3px;
  background: var(--rule);
  margin-block-end: var(--s-3);
  overflow: hidden;
}

.ramp::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--ink);
  transform-origin: left center;
  /* Full at rest. The scroll timeline takes it back to nothing and fills it
     again, so a browser that has never heard of a scroll timeline shows a
     finished bar rather than an empty one. */
  transform: scaleX(1);
}

[dir="rtl"] .ramp::after { transform-origin: right center; }

/* ---------------------------------------------------------------- tally --- */

/* One number, at headline size. It is real text in the markup and is legible
   with every stylesheet and script switched off. */
.tally {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  flex-wrap: wrap;
  max-inline-size: 48ch;
}

.tally__num {
  font-variation-settings: "wdth" var(--wdth-display);
  font-weight: 700;
  font-size: var(--t-h1);
  line-height: 1;
  letter-spacing: var(--track-h1);
  font-variant-numeric: tabular-nums slashed-zero;
}

.tally__body { flex: 1 1 22ch; min-inline-size: 0; }

/* ------------------------------------------------------------------ fit --- */

/* Who this is for, and who it is not. The two columns are told apart by ink
   density rather than by a tick against a cross: the left column's rules are
   full-strength ink and the right column's are the quiet rule colour, which is
   the same device the five passes use to carry their sequence. Two icon glyphs
   would have said it faster and would have been the only icons on the site. */
.fit {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  gap: var(--s-4) var(--gutter);
  min-inline-size: 0;
}

.fit__col:nth-child(1) { grid-column: 1 / span 5; }
.fit__col:nth-child(2) { grid-column: 7 / span 5; }

@media (max-width: 900px) {
  .fit__col:nth-child(1),
  .fit__col:nth-child(2) { grid-column: 1 / -1; }
}

.fit h3 {
  font-size: var(--t-body);
  font-variation-settings: "wdth" var(--wdth-annot);
  margin-block-end: var(--s-3);
}

.fit li {
  border-block-start: 2px solid var(--ink);
  padding-block: var(--s-2);
  font-size: var(--t-small);
  line-height: var(--lh-small);
}

.fit--not h3,
.fit--not li { color: var(--ink-soft); }
.fit--not li { border-block-start-color: var(--rule); }

/* ------------------------------------------------------------------- qa --- */

/* The questions a buyer asks on the call before they ask anything else. Answering
   them on the page is not generosity, it is the cheapest possible filter. */
.qa {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  gap: var(--s-5) var(--gutter);
  min-inline-size: 0;
}

.qa__item {
  grid-column: span 6;
  border-block-start: 2px solid var(--rule-strong);
  padding-block-start: var(--s-3);
  min-inline-size: 0;
}

@media (max-width: 900px) { .qa__item { grid-column: 1 / -1; } }

.qa__item h3 { font-size: var(--t-body); font-variation-settings: "wdth" var(--wdth-annot); }
.qa__item p { margin-block-start: var(--s-2); font-size: var(--t-small); line-height: var(--lh-small); }

/* ---------------------------------------------------------------- facts --- */

/* The studio stated as values rather than as prose, so the one page that claims
   to say who you are hiring can be checked in about four seconds. Every value is
   read out of studio.json, and any field left empty there drops its whole row
   rather than printing a label with nothing after it. */
.facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 0 var(--s-3);
  margin-block-start: var(--s-4);
  max-inline-size: 56ch;
}

.facts dt,
.facts dd {
  border-block-start: 2px solid var(--rule);
  padding-block: var(--s-2);
}

.facts dt {
  font-family: var(--font-mono);
  font-size: var(--t-data);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-soft);
}

.facts dd { font-size: var(--t-small); line-height: var(--lh-small); }

/* --------------------------------------------------------------- passes --- */

.passes {
  display: grid;
  grid-column: 2 / span 10;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--gutter);
  min-inline-size: 0;
}

@media (max-width: 900px) {
  .passes { grid-column: 1 / -1; grid-template-columns: 1fr; gap: var(--s-4); }
}

.pass {
  border-block-start: 3px solid var(--rule);
  padding-block-start: var(--s-2);
  min-inline-size: 0;
}

/* The rule above each pass darkens across the five, and that is what carries
   the sequence: delete the numbers and the order still reads. That was the
   test we set, and it is why the numerals stay small. */
.pass:nth-child(2) { border-block-start-color: color-mix(in oklab, var(--ink) 30%, var(--rule)); }
.pass:nth-child(3) { border-block-start-color: color-mix(in oklab, var(--ink) 55%, var(--rule)); }
.pass:nth-child(4) { border-block-start-color: color-mix(in oklab, var(--ink) 78%, var(--rule)); }
.pass:nth-child(5) { border-block-start-color: var(--ink); }

/* Pass two is where a person intervenes, and the accent marks that and nothing
   else in this section. */
.pass--human .pass__label { color: var(--accent); }

.pass__label {
  font-family: var(--font-mono);
  font-size: var(--t-data);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-block-end: var(--s-1);
}

.pass p { font-size: var(--t-small); line-height: var(--lh-small); }

/* ------------------------------------------------------------ schematic --- */

/* Dark on light, nothing thinner than 1.5px. The first version of this drawing
   was hairlines on a black band, which is the worst combination there is for an
   astigmatic reader: every line blooms and the drawing turns to fog. */
.schematic { min-inline-size: 760px; }

.schematic text {
  font-family: var(--font-mono);
  font-size: 12px;
  fill: var(--ink);
  letter-spacing: 0.03em;
}

/* Every weight here went up when the page went dark. A 1.5px line on paper is a
   construction line; the same line on a dark ground sits inside the halo of the
   label next to it and reads as a smudge, and a whole drawing of them reads as
   a grey wash. Nothing in this drawing is under 2px now. */
.schematic .construction { stroke: var(--rule-strong); stroke-width: 2; fill: none; }
.schematic .object { stroke: var(--ink); stroke-width: 2.6; fill: none; }
.schematic .redline { stroke: var(--accent); stroke-width: 3.2; fill: none; }
.schematic .redline-text { fill: var(--accent); }

/* --------------------------------------------------------------- drawings --- */

/* The schematic was one drawing on a site that argues for itself in prose, and a
   studio that says it draws things before it builds them should be able to show
   four of them. These are the rest of the family: the gap between a prototype and
   a product, the twenty-minute call, the ownership handover, and the tiers this
   studio works in. Same idiom, same two stroke weights, same single accent.

   The rule the accent follows here is the schematic's rule: it marks the one
   boundary or decision in a drawing and it never decorates. There is exactly one
   accent element in each of these files.

   Type is 15 units against a 900 viewBox with an 860px floor, so the smallest
   label lands at about 14.3px — the site's stated minimum. The schematic's own
   labels are 12 units and have always rendered below that floor; these do not
   repeat it. */
.dwg { min-inline-size: 860px; }

.dwg text {
  font-family: var(--font-mono);
  font-size: 15px;
  fill: var(--ink);
  letter-spacing: 0.02em;
  /* Labels now sit INSIDE the stroked groups they belong to, so that the
     stagger can move a cell and its own words together. Stroke inherits down an
     SVG tree, and without this every label would be outlined in the cell's
     stroke colour and turn to mud at small sizes. */
  stroke: none;
}

.dwg .t-soft { fill: var(--ink-soft); letter-spacing: var(--track-label); }
.dwg .t-accent { fill: var(--accent); }

.dwg .construction { stroke: var(--rule-strong); stroke-width: 2; fill: none; }
.dwg .object { stroke: var(--ink); stroke-width: 2.6; fill: none; }
.dwg .redline { stroke: var(--accent); stroke-width: 3.2; fill: none; }

/* Hovering a cell promotes it from construction weight to object weight, which
   is the drawing's own way of saying "this one is the subject". It deliberately
   does NOT touch opacity or transform: those two are driven by the scroll
   timeline, an animation beats a hover rule in the cascade, and the hover would
   simply never apply. Stroke is free, so stroke is what moves. */
.dwg--gap .cells > g path,
.dwg--gap .cells > g text { transition: stroke var(--t-quick) var(--ease-out), fill var(--t-quick) var(--ease-out); }
.dwg--gap .cells > g:hover path { stroke: var(--ink); stroke-width: 2.6; }
.dwg--gap .cells > g:hover text { fill: var(--ink); }

/* A drawing is a figure, so it gets a caption rather than a floating paragraph
   that happens to sit under it. */
.figure { margin-block-start: var(--s-4); }
.figure figcaption {
  margin-block-start: var(--s-2);
  color: var(--ink-soft);
  max-inline-size: 62ch;
}

/* -------------------------------------------------------------- colophon --- */

.colophon {
  border-block-start: 2px solid var(--rule);
  padding-block: var(--s-5) var(--s-4);
  font-size: var(--t-small);
}

.colophon__grid {
  display: grid;
  gap: var(--s-4) var(--gutter);
  grid-template-columns: repeat(12, minmax(0, 1fr));
  max-inline-size: var(--grid-max);
  margin-inline-end: auto;
}

.colophon__grid > *:nth-child(1) { grid-column: 1 / span 5; }
.colophon__grid > *:nth-child(2) { grid-column: 6 / span 3; }
.colophon__grid > *:nth-child(3) { grid-column: 9 / span 4; }

@media (max-width: 900px) {
  .colophon__grid > * { grid-column: 1 / -1 !important; }
}

.colophon h2 {
  font-size: var(--t-data);
  font-family: var(--font-mono);
  font-variation-settings: normal;
  font-weight: 400;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-block: 0 var(--s-2);
}

.colophon ul li + li { margin-block-start: var(--s-1); }

#motion-toggle {
  font: inherit;
  font-size: var(--t-data);
  font-family: var(--font-mono);
  background: transparent;
  color: var(--ink-soft);
  border: 2px solid var(--rule-strong);
  border-radius: var(--r-2);
  padding: 10px 14px;
  min-block-size: 48px;
  cursor: pointer;
}

#motion-toggle:hover,
#motion-toggle:focus-visible { color: var(--ink); border-color: var(--ink); }
#motion-toggle[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }

/* ----------------------------------------------------------------- form --- */

.field { display: grid; gap: 8px; margin-block-end: var(--s-3); max-inline-size: 44ch; }

label {
  font-variation-settings: "wdth" var(--wdth-annot);
  font-size: var(--t-small);
  font-weight: 500;
}

input[type="text"],
input[type="email"],
textarea {
  font: inherit;
  font-size: var(--t-body);
  padding: 12px 14px;
  min-block-size: 52px;
  background: var(--paper);
  color: var(--ink);
  border: 2px solid var(--rule-strong);
  border-radius: var(--r-1);
}

textarea { min-block-size: calc(var(--lead) * 4); resize: vertical; }
input:focus-visible, textarea:focus-visible { border-color: var(--ink); }
input:user-invalid, textarea:user-invalid { border-color: var(--accent); }
.error { color: var(--accent); font-size: var(--t-small); }

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

/* ARRIVE — a section reaching its place. Once, on entry, and only on the four
   blocks that open a section. Nothing here is opacity:0 at rest: the hidden
   starting state exists only inside html.js, inside a no-preference query,
   inside a @supports, so the JavaScript, the preference and the capability must
   all hold before a single element on this page is hidden. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    html.js .arrive {
      animation: arrive var(--t-arrive) var(--ease-out) both;
      animation-timeline: view();
      animation-range: entry 8% cover 34%;
    }
  }
}

/* Transform only. No opacity.

   Two reasons, and the second is the important one. A scroll-driven fade leaves
   everything below the fold sitting at opacity 0 until it is scrolled to, which
   is content that is genuinely invisible rather than merely un-animated — bad
   for anyone who cannot scroll, and it breaks the promise made at the top of
   this file. And `opacity: 0` plus `translateY(20px)` is the single most
   recognisable motion on the web right now; it appears on roughly four in five
   generated pages, applied uniformly, creating no hierarchy at all.

   So a section settles into place instead of appearing. It is always readable,
   at every scroll position, with or without the animation having run. */
@keyframes arrive {
  from { transform: translate3d(0, 22px, 0); }
  to { transform: none; }
}

/* ------------------------------------------------------ scroll-driven --- */

/* The drawings animate by being drawn.

   Every animation below is scrubbed against a native CSS scroll timeline, so
   the position of the page IS the position of the animation: scroll back up and
   the line un-draws. No library, no scroll listener, no rAF loop, and no bytes.

   The whole block is fenced three deep — @supports, prefers-reduced-motion, and
   html.js — so a browser without scroll timelines, a reader who has asked for
   less motion, and a visitor with no JavaScript each get the finished state
   rather than a broken one. That fence is why none of this can leave content
   stranded mid-animation. */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* Each figure is its own clock. Descendants name it rather than measuring
       themselves, which is what lets nine cells inside one SVG stagger against
       a single traversal instead of all firing the moment the SVG appears. */
    html.js .figure--live {
      view-timeline-name: --fig;
      view-timeline-axis: block;
    }

    /* A line arriving along its own length. pathLength="1" in the markup means
       this works on any path in any drawing without the stylesheet knowing how
       long the line is. */
    html.js .draws path,
    html.js .dwg .draw {
      stroke-dasharray: 1;
      stroke-dashoffset: 1;
      animation: draw-in linear both;
      animation-timeline: --fig;
      animation-range: entry 16% cover 46%;
    }

    /* The nine obligations, one after another, left to right and top to bottom.
       Opacity is allowed here and nowhere near a paragraph: this is a picture,
       and a reader who cannot scroll loses a picture rather than an argument. */
    html.js .dwg--gap .cells > g {
      opacity: 0;
      transform: translate3d(0, 12px, 0);
      animation: cell-in linear both;
      animation-timeline: --fig;
    }

    html.js .dwg--gap .cells > g:nth-child(1) { animation-range: entry 30% entry 62%; }
    html.js .dwg--gap .cells > g:nth-child(2) { animation-range: entry 34% entry 66%; }
    html.js .dwg--gap .cells > g:nth-child(3) { animation-range: entry 38% entry 70%; }
    html.js .dwg--gap .cells > g:nth-child(4) { animation-range: entry 42% entry 74%; }
    html.js .dwg--gap .cells > g:nth-child(5) { animation-range: entry 46% entry 78%; }
    html.js .dwg--gap .cells > g:nth-child(6) { animation-range: entry 50% entry 82%; }
    html.js .dwg--gap .cells > g:nth-child(7) { animation-range: entry 54% entry 86%; }
    html.js .dwg--gap .cells > g:nth-child(8) { animation-range: entry 58% entry 90%; }
    html.js .dwg--gap .cells > g:nth-child(9) { animation-range: entry 62% entry 94%; }

    /* The process, filling as you read it. */
    html.js .ramp::after {
      animation: ramp-fill linear both;
      animation-timeline: view();
      animation-range: entry 60% cover 52%;
    }

    /* The one number on the page, arriving with some weight behind it. It is
       real text at rest, so it survives every fence above. */
    html.js .tally__num {
      animation: tally-in var(--t-arrive) var(--ease-out) both;
      animation-timeline: view();
      animation-range: entry 20% cover 30%;
    }
  }
}

@keyframes draw-in { to { stroke-dashoffset: 0; } }
@keyframes cell-in { to { opacity: 1; transform: none; } }
@keyframes ramp-fill { to { transform: scaleX(1); } }
@keyframes tally-in {
  from { transform: translate3d(0, 26px, 0) scale(0.82); }
  to { transform: none; }
}

/* Cross-document view transitions. No router, no library, real URLs. */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation: page-out var(--t-page) var(--ease-in); }
::view-transition-new(root) { animation: page-in var(--t-page) var(--ease-out); }
@keyframes page-out { to { opacity: 0; } }
@keyframes page-in { from { opacity: 0; } }

/* Durations go to 0.01ms rather than `none`, so transitionend still fires and
   no state machine hangs waiting for an event that will never arrive. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

html[data-motion="reduced"] *,
html[data-motion="reduced"] *::before,
html[data-motion="reduced"] *::after {
  animation-duration: 0.01ms !important;
  transition-duration: 0.01ms !important;
}

.js-only { display: none; }
html.js .js-only { display: revert; }
html.js span.js-only { display: inline; }
html.js .no-js-only { display: none; }

/* ---------------------------------------------------------------- print --- */

@media print {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    opacity: 1 !important;
  }
  canvas, .masthead nav, #motion-toggle { display: none; }
  body { background: transparent; color: var(--ink); }
  .band { padding-block: 1.5rem; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.8em; }
}
