/* Colour tokens. Structure from @primer/primitives 11.10.0 (MIT),
   read from shared/tokens/primer.json - do not edit these values by hand. The accent is not
   Primer's; it is the brand teal, and it is set in shared/tokens.mjs.

   Three blocks and one rule between them: the light theme is the default, the dimmed theme answers
   a system preference, and [data-theme] is the reader's own choice and outranks both. The attribute
   is written onto <html> before first paint by the theme script in shared/theme.mjs. */

:root {
  color-scheme: light dark;
  --canvas: #ffffff;
  --canvas-subtle: #f6f8fa;
  --canvas-inset: #f6f8fa;
  --neutral-muted: #818b981f;
  --border: #d1d9e0;
  --border-muted: #d1d9e0b3;
  --fg: #1f2328;
  --fg-muted: #59636e;
  --control-border: #818b98;
  --shadow-small: 0 1px 1px 0 #1f23280a, 0 1px 2px 0 #1f232808;
  --accent: #0f766e;
  --fg-on-accent: #ffffff;
  --theme-on: 0;
  --wash-near: 9%;
  --wash-far: 5%;
  --decor-tint: 13%;
  --decor-base: var(--canvas);
}

@media (prefers-color-scheme: dark) {
  :root {
    --canvas: #212830;
    --canvas-subtle: #262c36;
    --canvas-inset: #151b23;
    --neutral-muted: #656c7633;
    --border: #3d444d;
    --border-muted: #3d444db3;
    --fg: #d1d7e0;
    --fg-muted: #9198a1;
    --control-border: #656c76;
    --shadow-small: 0 1px 1px 0 #01040999, 0 1px 3px 0 #01040999;
    --accent: #3fcfbd;
    --fg-on-accent: #151b23;
    --theme-on: 1;
    --wash-near: 3%;
    --wash-far: 2%;
    --decor-tint: 28%;
    --decor-base: var(--decor-deep);
  }
}

:root[data-theme="light"] {
  color-scheme: light;
  --canvas: #ffffff;
  --canvas-subtle: #f6f8fa;
  --canvas-inset: #f6f8fa;
  --neutral-muted: #818b981f;
  --border: #d1d9e0;
  --border-muted: #d1d9e0b3;
  --fg: #1f2328;
  --fg-muted: #59636e;
  --control-border: #818b98;
  --shadow-small: 0 1px 1px 0 #1f23280a, 0 1px 2px 0 #1f232808;
  --accent: #0f766e;
  --fg-on-accent: #ffffff;
  --theme-on: 0;
  --wash-near: 9%;
  --wash-far: 5%;
  --decor-tint: 13%;
  --decor-base: var(--canvas);
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --canvas: #212830;
  --canvas-subtle: #262c36;
  --canvas-inset: #151b23;
  --neutral-muted: #656c7633;
  --border: #3d444d;
  --border-muted: #3d444db3;
  --fg: #d1d7e0;
  --fg-muted: #9198a1;
  --control-border: #656c76;
  --shadow-small: 0 1px 1px 0 #01040999, 0 1px 3px 0 #01040999;
  --accent: #3fcfbd;
  --fg-on-accent: #151b23;
  --theme-on: 1;
  --wash-near: 3%;
  --wash-far: 2%;
  --decor-tint: 28%;
  --decor-base: var(--decor-deep);
}

/* The hand-written half of /site.css. The colour tokens above this point are generated from
   shared/tokens.mjs; everything below spends them.

   Four rules this file is written under:

   1. Nothing here is load-bearing for reading the page. With the stylesheet missing the document
      is still a heading, some paragraphs and some lists, in order.
   2. No rule may wait for a class to be added at runtime. There is one script on these sites -
      shared/theme.mjs, which remembers a reader's choice of light or dark - and the single
      attribute it writes, `data-theme` on <html>, is written before the body is parsed and is read
      only by the generated token blocks above this file. Nothing here waits for it: with it absent
      the tokens are the light theme and the system preference, which is what every rule below
      spends. Anything else that would need script is not built.
   3. Every modern feature used below degrades. A browser that supports none of them still gets
      the whole content, the right hierarchy and legible contrast; where the fallback would look
      broken instead of plain, the rule sits behind @supports.
   4. The teal means something. It appears on the mark, on the single primary action, on the cursor
      typing the headline and on the one word of it that changes, on the cursor typing the
      transcript on kingshand, on the three engagement icons, on the focus ring, on the link or
      showcase tile under the pointer, and - at a fraction of its strength - in the abstract shapes
      behind the page. Nowhere else. An accent spent everywhere is not an accent. The mark is a
      cursor bar, so the headline's cursor and the mark are the same object saying the same thing,
      which is why that is one place and not two - and the transcript's caret is the third thing
      that same object is, on the one page where a terminal is what is being drawn. The rest of the
      headline's letters are the ordinary foreground: a two-line display heading in teal would be the
      page shouting, and it would take the one teal word's job away from it.

   Motion is off for anyone who asks for less of it, at the bottom of this file. */

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

   Committed to this repository and served from this origin. Nothing on any page makes a request
   to a third party, so a self-hosted file is the only kind of web font allowed here. The italic is
   a real cut rather than a slant the browser invents, and it carries every <em> in the mirrored
   documentation. The mono is here for two jobs: labels and code, and the typed headline, which
   needs a face where every character is exactly one ch wide. Licences sit beside the files. */

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/inter-latin-wght-normal.woff2") format("woff2");
}

@font-face {
  font-family: "Inter";
  font-style: italic;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/inter-latin-wght-italic.woff2") format("woff2");
}

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

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

/* ---------------------------------------------------------------- scale

   One type scale and one spacing scale, both stated once. Every size above the base is fluid, so
   the page breathes between a 320px phone and a 2560px monitor instead of stepping at a
   breakpoint. Each clamp keeps a rem term alongside the vw term: a scale built on vw alone stops
   growing when somebody zooms to 200%, which is the one time it matters most. */

:root {
  --font-sans: "Inter", ui-sans-serif, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, "Liberation Mono", monospace;

  --text-xs: 0.8125rem;                                    /* 13px - mono labels, captions */
  --text-sm: 0.9375rem;                                    /* 15px - navigation, notes, facts */
  --text-base: clamp(1rem, 0.97rem + 0.15vw, 1.125rem);    /* 16 - 18px - prose */
  --text-md: clamp(1.1875rem, 1.09rem + 0.42vw, 1.4375rem);/* 19 - 23px - standfirst */
  --text-lg: clamp(1.375rem, 1.22rem + 0.7vw, 1.75rem);    /* 22 - 28px - h3 */
  --text-xl: clamp(1.625rem, 1.35rem + 1.2vw, 2.25rem);    /* 26 - 36px - h2 */
  --text-h1: clamp(2rem, 1.65rem + 1.6vw, 2.875rem);
  --text-display: clamp(2.25rem, 1.5rem + 3.1vw, 4.25rem);

  /* Multiples of 4px, so vertical distances land on one rhythm rather than near it. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;
  --space-9: 5rem;

  /* The two numbers that decide where anything is horizontally, on every page of every site: the
     content column, and the space between it and the window. Nothing else may set a width, a
     horizontal margin or a horizontal padding on a page shell, a section or a block inside one -
     see the layout primitives below, which are the only rules that spend these. Changing the width
     of every site is changing --page, here, once. */
  --page: 64rem;
  --gutter: var(--space-5);
  --radius: 6px;
  --radius-lg: 14px;
  --speed: 140ms;
  --ease: cubic-bezier(0.2, 0, 0.13, 1);
}

* { box-sizing: border-box; }

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

/* No horizontal padding here. The gutter used to be body's, which meant the page's edges were
   decided in one place and its width in another, and the two had to be read together to know where
   anything was. Both are `.page` below now. What is left here is vertical. */
body {
  margin: 0;
  padding-bottom: var(--space-9);
  background: var(--canvas);
  color: var(--fg);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: 1.65;
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
}

/* The canvas is tinted rather than flat: two very soft washes of the accent, fixed behind
   everything, so the page has a light source instead of a blank sheet. The layer is fixed, so it
   sits under body text everywhere, not just the hero.

   These are two background layers and they composite, so the strength under any given pixel is
   the pair stacked, not either percentage on its own - and the worst point is not a gradient
   centre. The radii are absolute rem while the centres are percentages, so on a narrow viewport
   (320x640 is the worst of the sizes checked) both ellipses cover the whole width and their
   strengths add near the top edge. Stacked there the dark pair reaches 3.8% accent, where
   --fg-muted holds 4.75:1, and the light pair reaches 10.4%, where it holds 5.29:1; the floor is
   4.5:1. At 4%/3% the dark stack was 5.4% accent and 4.61:1 - still passing, but on the margin.
   Changing either percentage means re-measuring the stack at a narrow viewport, not the layer
   being edited. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

/* Every wash is behind the feature query rather than relying on the cascade to step back from it.
   A value holding var() is pending-substitution at parse time, so it wins the cascade and is only
   found to be invalid at computed-value time - at which point the property takes its initial
   value, not the declaration above it. A UA without color-mix() therefore gets no tint at all
   here, which is the plain result rule 3 asks for.

   The pair is stated once, as `--wash`, because the sticky header has to repaint it. That header is
   opaque - it has to be, or the page scrolls through it - and painted in the flat canvas it would be
   a rectangle of untinted colour laid over the strongest corner of the wash, with the tint still
   showing in the gutters on either side of it. So the header paints the canvas and then this on top
   of it, `background-attachment: fixed`, whose positioning area is the viewport - the same box the
   layer below is fixed to. The two therefore align exactly and the header is invisible against what
   it covers. Two consumers, one definition: editing this changes both.

   The two strengths are `--wash-near` and `--wash-far`, which are theme tokens - see SHAPE in
   shared/tokens.mjs. This block used to be written twice, the second copy inside
   `@media (prefers-color-scheme: dark)`, and that stopped being right the day a reader could choose
   a theme: a media query cannot see a choice, so a dimmed page chosen on a light machine would have
   been painted at the light theme's 9% and 5% - which is the pair measured against light text on a
   light canvas, not against near-white text on a dark one. As a token the strength follows the same
   cascade the colours do and there is one place that answers which theme this is. */
@supports (color: color-mix(in oklab, red 10%, white)) {
  :root {
    --wash:
      radial-gradient(70rem 45rem at 12% -12%,
        color-mix(in oklab, var(--accent) var(--wash-near), transparent), transparent 70%),
      radial-gradient(50rem 40rem at 96% 2%,
        color-mix(in oklab, var(--accent) var(--wash-far), transparent), transparent 66%);
  }

  body::before { background-image: var(--wash); }
}

/* ---------------------------------------------------------------- abstract decoration

   Three shapes behind the apex landing page: a dot field in the hero's empty right-hand side, and
   two soft pools bleeding in from the window's edges further down. Written here as gradients rather
   than fetched as an image, so the zero-external-request property survives, and none of it moves.
   The page already has a typing headline and two travelling strips; a third moving thing would be
   noise. The same three shapes as pass 4 built - what changed in pass 8 is how strong they are, and
   the reason is below.

   Scoped to `body.landing`, which only that page carries. This stylesheet is shared with the
   kingshand subdomain, and a tool's documentation is not the place for a company's decoration - it
   would also put a dot field behind a page whose hero this was never measured against.

   Every shape is a pseudo-element, so there is nothing in the markup to hide and nothing to
   announce - delete this block and the page reads identically. They sit behind their content at a
   negative z-index and take no pointer events.

   Two properties make the contrast question answerable rather than a hope.

   The pools are opaque at the core and fade to nothing, so every pixel under one lies on the straight
   segment between that opaque colour and whatever the canvas wash beneath it already was. Contrast
   against a fixed text colour is monotone along such a segment as long as the text's own luminance is
   not between the two ends - and it is nowhere near either end in either theme - so checking the two
   ends checks the whole gradient. A translucent teal veil would not have that property: it would add
   tint on top of the wash wherever the wash happened to be strongest, and that point moves with the
   viewport.

   ------------------------------------------------------ how strong the pools are, and why

   For four passes this was 9% of the accent into the canvas in light and 10% into the inset in the
   dimmed theme, and the King's verdict was the only one that matters: "I dont see any designs on
   screen, you earlier asked me abstract images vs real and we choose abstract, where is the output
   for that?" It was there and nobody could see it. The dimmed value was the worse of the two and for
   a reason worth writing down: the accent mixed into the *inset* at 10% lands at rgb(27,41,47) on a
   canvas of rgb(33,40,48). It had been tuned until it matched the thing it was supposed to differ
   from.

   So the dimmed pool no longer goes lighter, it goes **deeper**: the accent mixed into the inset
   deepened toward black. That is the move that buys everything, because in a dark theme every text
   colour on the page is lighter than the canvas, so a darker backdrop is more contrast rather than
   less. The pool is now rgb(18,49,47) - fifteen points of red below the canvas and nine of green
   above it, a teal a reader can plainly see - and the weakest text on the site, --fg-muted, holds
   4.79:1 on it against a floor of 4.5. At the old 10% it held 5.13:1 and was invisible. That is the
   trade, and it is measured rather than reasoned: every number in this comment came out of a pixel
   scan of the rendered pages, not out of this file.

   Light went from 9% to 13% and stopped there, and what stopped it is not --fg-muted, which holds
   5.11:1 at 13% and would hold 4.72:1 at 18%. It is the accent used as *text*: a link under the
   pointer is drawn in the accent, and the accent on a 13% pool is 4.57:1 where an 18% pool would put
   it at 4.22:1 - below the floor, in a state a reader can reach. 13% is the last value at which
   every state of every text colour on the page clears AA. Recorded in the design record for the King
   rather than decided here: if they will take teal-on-hover at 4.2:1 over a pool, light can go to
   18% and the tint roughly doubles.

   The pools now run at every width, which is a change from pass 4. That pass gave a phone no
   decoration on the grounds that there was no empty space to fill, and that reasoning was right about
   the dot field and wrong about these: a pool is not an object competing for room, it is a light
   source behind the page, and a 390px screen has as much use for one as a monitor. The dot field
   stays above 64rem, because it *is* an object and a phone has no empty corner to put it in. That is
   also what happens when a reader zooms: rem does not scale with zoom, so the query fails before the
   type grows.

   The dot field is additive rather than substitutive, so it is placed rather than argued about, and
   at 45% of the accent it is now a texture a reader can see instead of a rumour. It is placed where
   no letter reaches: at 64rem and up the hero's text - a headline eighteen monospaced characters
   wide, a one-line standfirst, two buttons - comes nowhere near the right-hand quarter the dots
   occupy, and the mask keeps the dots themselves in the top-right corner of even that. That is
   checked by counting painted text pixels inside the field's own box at every width and both themes
   rather than by looking at it: none.

   `screen` does two jobs. Paper gets no decoration at all, and it needs no print rule to take it
   away again. A browser that fails either feature query gets none either, which is a plain readable
   page that has lost nothing. The two queries differ on purpose: a pool is a gradient and needs only
   color-mix(), while the dot field would be a hard-edged rectangle of dots without a mask, so only
   that one waits for mask-image. */

@media screen {
  @supports (color: color-mix(in oklab, red 10%, white)) {
    :root {
      /* The inset, deepened. Stated as its own step rather than folded into one nested mix, because
         the two halves answer different questions: this one is how dark the pool is, and the one
         below is how teal. Declared in both themes and read only by the dimmed one, which names it
         as its `--decor-base`; it has to be a declaration here rather than a token in
         shared/tokens.mjs, because a color-mix() belongs behind the feature query for one. */
      --decor-deep: color-mix(in oklab, var(--canvas-inset) 45%, #000);
      /* One pool, two themes. `--decor-tint` and `--decor-base` are theme tokens - 13% into the
         canvas in light, 28% into the deepened inset in the dimmed theme - so this is a single rule
         rather than a media query that could not see a reader's choice. See SHAPE in
         shared/tokens.mjs, and the measurements below for what those two numbers cost. */
      --decor-pool: color-mix(in oklab, var(--accent) var(--decor-tint), var(--decor-base));
    }

    /* The pools are anchored to the document rather than to the window, so they sit at places on the
       page instead of following the reader down it. `inset: 0` is body's padding box, so the layer
       cannot make the document taller or wider than it already is - and body has no horizontal
       padding since the gutter moved into `.page`, so the pools now bleed from the window's own
       edges rather than from inside a gutter. */
    body.landing { position: relative; }

    body.landing::after {
      content: "";
      position: absolute;
      inset: 0;
      z-index: -1;
      pointer-events: none;
      /* Larger than they were, and holding their core for longer before the fade starts. Intensity
         alone was not the whole of why nobody could see these: a faint colour over a small area
         reads as a rendering artefact, and the same colour over a third of the page reads as a
         decision. */
      background:
        radial-gradient(44rem 34rem at -4% 38%, var(--decor-pool), transparent 72%),
        radial-gradient(40rem 32rem at 104% 76%, var(--decor-pool), transparent 70%);
    }
  }
}

@media screen and (min-width: 64rem) {
  @supports (color: color-mix(in oklab, red 10%, white)) and (mask-image: radial-gradient(#000, transparent)) {
    :root { --decor-dot: color-mix(in oklab, var(--accent) 45%, var(--canvas)); }

    /* The dot field, in the hero's own right-hand corner. `main` is already a containing block
       through its container-type, but that is stated here rather than relied on. */
    .landing main { position: relative; }

    .landing main::before {
      content: "";
      position: absolute;
      top: 0;
      right: 0;
      z-index: -1;
      width: min(17rem, 30%);
      height: 11rem;
      pointer-events: none;
      background-image: radial-gradient(var(--decor-dot) 1.2px, transparent 1.3px);
      background-size: 1.35rem 1.35rem;
      /* Anchored at the corner, so the dots thin out toward the text rather than stopping at an
         edge. Without the mask this would be a rectangle of dots, which is why this block is behind
         a feature query for it. */
      mask-image: radial-gradient(120% 120% at 100% 0, #000 0%, transparent 65%);
    }
  }
}

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

   Three classes, and between them they own every horizontal decision any of these sites makes.
   Nothing else in this file may set a width, a horizontal margin or a horizontal padding on a page
   shell, a section, or a block that is a section's own child.

   This is the pass that made that a rule. Every width complaint this project has had - the FAQ
   answers, the footer paragraph, two section intros, the closing line of the contact section, the
   strips running to the screen edges - was the same fault reported five times: the design system had
   tokens for colour, type and space and no layout primitives, so each section was hand-built with
   its own width behaviour and no single rule decided anything. Five instructions, five one-element
   fixes, and the sixth section would have needed a sixth. The King: "not sure why we are not making
   sections and divs and reusing classes for all sections as it was a total waste of 2-3 hours,
   fixing stupid stuff."

     .page     the content column. Carried by <header>, <main>, <footer> and the box holding the
               skip link on every page of every site, written once in shared/layout.mjs, so a page
               cannot forget it.
     .section  a top-level division of that column. Takes the full column and never narrows it;
               supplies the vertical rhythm and the rule that tells one section from the next.
     .columns  a row of equal columns inside a section. The four grids there were when this class
               was written - the products, the engagement tiles, the three steps under the contact
               invitation, the footer groups - were four copies of one declaration differing only in
               the width a column collapses at. The screenshot row is the fifth, and it inherited
               all of that by carrying the class.

   Why the column is stated as `--page` plus two gutters rather than as a max-width on its own: the
   gutter is inside this box, so the content column is exactly `--page` wide on a large screen and
   exactly the window less two gutters on a small one. Written as a bare `max-width: var(--page)`
   with the gutter on <body>, the page would be `--page` wide on a monitor and `--page` plus two
   gutters overall, and the number in the token would not be the number on the screen.

   There is no full-bleed class. Nothing on any site is full-bleed any more - the strips were,
   for one pass, and the King rejected it: "this is taking full width when other parts taking less".
   A named class nothing carries is a class the next section reaches for by accident, so when
   something does need to escape the column it gets one, written here, and not a margin of its own.

   There is no prose measure either, and that is a ruling rather than an omission. Every paragraph
   on every site runs the full column. It was 37rem, about seventy characters, which is the width
   the typographic advice gives - and the King has now asked five times for the full width instead
   and settled it: "I dont think any section's width should differ, all will be same." At the 1024px
   column that is around 120 characters a line. The trade is recorded where the King can see it, and
   this is the version they asked for. */

.page {
  width: 100%;
  max-width: calc(var(--page) + 2 * var(--gutter));
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* The rule and the gap belong to the join between two sections rather than to a section, which is
   what lets the first one - the hero, which needs no line above it - carry the same class as the
   rest without a reset undoing anything. */
.section + .section {
  margin-top: var(--space-8);
  padding-top: var(--space-8);
  border-top: 1px solid var(--border-muted);
}

.section > h2 { margin-top: 0; }

/* As many tracks as the section will hold at `--column-min`, collapsing to one on a phone. auto-fit
   rather than auto-fill on purpose: it drops the tracks nothing was put in, so one product tile
   fills the row instead of sitting at a track's width with the rest of the row blank beside it -
   which is what the King called garbage, and they were right. min() is what keeps a track from
   outgrowing a narrow phone. The gap is the default and a row may set its own; the widths are the
   row's business, through the one custom property, and never a grid declaration of its own. */
.columns {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--column-min, 15rem)), 1fr));
}

/* Without color-mix() the browser's own highlight stands, which is plain rather than broken. */
@supports (color: color-mix(in oklab, red 10%, white)) {
  ::selection { background: color-mix(in oklab, var(--accent) 22%, transparent); }
}

/* ---------------------------------------------------------------- chrome */

/* The chip has no horizontal geometry of its own, and must not: it sits in a `.page` box written in
   shared/layout.mjs, so the first thing the keyboard reaches starts on the same left edge as the
   wordmark below it. Out of focus it is out of flow, so that box is zero pixels tall. */
.skip-link { position: absolute; left: -9999px; }

.skip-link:focus {
  position: static;
  display: inline-block;
  margin-top: var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: var(--canvas-subtle);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* The header stays at the top of the window while the page scrolls, on every site.
 *
 * The King: "header menu is not fixed, on scroll its gone. so whats the meaning of putting logo on
 * scroll there if menu is not visible." The wordmark that fades in over the first stretch of scroll
 * was fading into a bar that had already left the screen, and the navigation went with it. Sticky is
 * what makes both of them mean something: a bare header over the hero, the wordmark and the nav
 * travelling with the reader after that.
 *
 * The subdomains stick too. A subdomain header is links and no wordmark reveal, so nothing about
 * the fade applies there - but kingshand's documentation pages are the longest documents any of
 * these sites serves, and a navigation that scrolls away is worth less on those than on the landing
 * page. One rule, every site, which is also one less thing that behaves differently between them.
 *
 * Four things it has to get right, and none of them is the `position` line:
 *
 * - **Opaque.** Text scrolls through a transparent sticky header. The background is the canvas, and
 *   the wash on top of it - see `--wash` above - so the bar matches what it covers rather than
 *   printing a flat rectangle over the tinted corner of the page. The existing hairline stays as the
 *   edge; nothing else is added to say the bar is there.
 * - **Above everything.** The decoration all sits at a negative z-index, so any positive one clears
 *   the dot field and both pools; 10 also clears the sections, which are unpositioned, and the one
 *   positioned box on any of these sites - the tilted transcript card - which has no z-index of its
 *   own.
 * - **No jump.** `sticky` leaves the element in flow, so the page does not shift by the header's
 *   height at the moment it sticks. `fixed` would, and that is the whole reason this is not fixed.
 * - **Anchors clear it.** Every in-page link on this site lands on a heading, and a sticky header
 *   covers the heading it lands on unless the target reserves room for it. `--header-height` below
 *   is what does that, and it is measured rather than guessed - see the scroll-margin rule.
 *
 * Sticky is position, not motion, so a reader who asked for less motion still gets it. Paper has no
 * viewport to stick to and the print sheet puts it back in flow. */
header {
  position: sticky;
  top: 0;
  z-index: 10;
  background-color: var(--canvas);
  display: flex;
  flex-wrap: wrap;
  /* The wordmark at one edge and everything else at the other. Unconditional, because the far end
     is: shared/layout.mjs writes `.header-end` into every header, so there is always a second child
     that belongs against the far margin whether or not the site declares any navigation. */
  justify-content: space-between;
  gap: var(--space-2) var(--space-5);
  align-items: baseline;
  /* Vertical only. The bar carries `.page`, so its horizontal padding is the page's gutter and is
     not this rule's to state - a `padding` shorthand here would be a second opinion about it. */
  padding-block: var(--space-5) var(--space-4);
  border-bottom: 1px solid var(--border-muted);
}

/* The wash, repainted on the bar in the viewport's own coordinates so it lines up with the fixed
   layer underneath. Behind the same feature query as the layer it is copying: a UA without
   color-mix() has no wash to match, and the flat canvas is then exactly right. */
@supports (color: color-mix(in oklab, red 10%, white)) {
  header {
    background-image: var(--wash);
    background-attachment: fixed;
  }
}

.brand {
  margin: 0;
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: -0.022em;
}

.brand a {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4em;
  color: inherit;
  text-decoration: none;
}

/* The mark: a teal cursor bar set before the wordmark. Drawn rather than fetched, so it scales
   with the type and costs no request. It says nothing a reader needs, which is why the markup
   marks it decorative. */
.brand-mark {
  width: 0.24em;
  height: 0.9em;
  border-radius: 1px;
  background: var(--accent);
  transform: translateY(0.05em);
}

header nav ul {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-sm);
}

header nav li { margin: 0; }
header nav a { color: var(--fg-muted); text-decoration: none; }
header nav a:hover { color: var(--fg); }
header nav [aria-current="page"] { color: var(--fg); font-weight: 600; }

/* One nav item is an action rather than a destination, so it is the same button the action rows
   use - restated only where the rules above would repaint it grey. Outlined, never filled: the
   page has one primary action and it is the hero's. */
header nav a.button {
  padding: 0.45em 0.95em;
  color: var(--fg);
}

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

/* ------------------------------------------------------ the theme toggle
 *
 * Light and dark, and a reader's choice of the two remembered across pages and across the
 * subdomains. The control is written into the header by shared/layout.mjs, so it is in the served
 * bytes rather than assembled at runtime; the script in shared/theme.mjs only writes the choice
 * onto <html> and keeps `aria-pressed` in step.
 *
 * `.header-end` is the navigation and this button as one box at the far end of the bar. The header
 * is `space-between` and had two children; a third would have pushed the navigation into the middle
 * of the row, which is a change to a settled design rather than an addition to it. Baseline
 * alignment inside it is why the button's first child is its label and not the switch - a flex
 * container takes its baseline from its first item, and an empty box has none to give.
 *
 * The switch is drawn rather than fetched, like the house mark, so it costs no request and scales
 * with the type. Which side the knob sits on is `--theme-on`, a theme token: 0 in light, 1 in dark.
 * That is the whole reason it is a token - the knob is then on the right side for a reader who has
 * chosen a theme *and* for one who has only a system preference, and nothing has to run for it to
 * be right. It is not drawn in the accent: the teal names the one action on a page, and a control
 * that changes nothing about the work is not it.
 *
 * The button is quieter than the header's Contact button on purpose. Contact is what this site is
 * asking anyone to do; this is a preference, and it sits at the same height so the row still reads
 * as one line of chrome. */

.header-end {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-5);
}

.theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  /* The Contact button's vertical padding, so the two sit at one height in the bar. */
  padding: 0.45em 0.8em;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: none;
  color: var(--fg-muted);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
  transition: color var(--speed) var(--ease), border-color var(--speed) var(--ease);
}

.theme-toggle:hover { color: var(--fg); border-color: var(--fg-muted); }

.theme-toggle-switch {
  position: relative;
  flex: none;
  width: 1.8em;
  height: 1em;
  border: 1px solid var(--control-border);
  border-radius: 1em;
}

.theme-toggle-switch::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0.15em;
  width: 0.6em;
  height: 0.6em;
  border-radius: 50%;
  background: currentcolor;
  /* 0 or 0.8em, which is the token doing the work. A browser that cannot resolve the calc leaves
     the knob at the left, which is a switch that is simply off rather than a broken one. */
  transform: translate(calc(var(--theme-on) * 0.8em), -50%);
  transition: transform var(--speed) var(--ease);
}

/* A link lands on something inside main, and the sticky header is sitting on top of the window - so
   the target has to reserve the bar's height or the heading it scrolls to is behind it.
 *
 * `main` itself is in the list because the skip link points at it: on kingshand that is the only
 * in-page link there is, and skipping the navigation to land underneath the navigation is the same
 * defect as a heading under the bar. Everything with an id inside main is in it because the two
 * strip headings carry ids too, and because the next id somebody adds should not have to remember
 * this rule.
 *
 * `--header-height` is an upper bound on the bar's height rather than its height, and it is measured
 * rather than estimated. It has to be an upper bound because every site shares this stylesheet and
 * their headers are not the same object: a heading that lands under the bar is a defect, and blank
 * space above one is not, so every value here rounds up and the failure is always in that direction.
 * A space-4 is added on top so the heading clears the hairline with a gap rather than touching it.
 *
 * The apex is tiered, because it has five in-page anchors and they are how the page is navigated:
 * 79px from 1024 up, 115px between about 480 and 560 where the toggle is the item that no longer
 * fits beside the navigation, and 98px below 480 where the bar's own padding and gaps close up and
 * the toggle loses its word. Both subdomains take one number for every width. kingshand's header is
 * three nav items, a toggle and no wordmark reveal, and it wraps to two rows by 460px and three by
 * 340, so tracking it would need three more tiers - and it has exactly one in-page target, the skip
 * link, whose whole job is to get past the navigation. Landing it further past than necessary costs
 * nothing anybody can see. The other subdomain does have in-page anchors of its own, so there the
 * slack is space above a heading rather than nothing at all - it is untiered anyway, because the
 * one number already clears its bar at every width, which tests/theme-browser.test.mjs measures
 * rather than assumes.
 *
 * Every number here was re-measured when the toggle was added, at fifteen widths on the two sites
 * there were then, and three of them moved: a control in the bar is one more item to wrap, and a
 * reservation that was right for the bar before it is a heading under the bar after it. */
:root { --header-height: 8.5rem; }
body.landing { --header-height: 5rem; }

/* The band where the apex bar is two rows: the navigation and the toggle no longer fit beside the
   wordmark, and the desktop padding and gaps are still in force. Below 30rem both close up and the
   narrow-screen block at the foot of this file takes over with a smaller number. */
@media (max-width: 36rem) {
  body.landing { --header-height: 7.5rem; }
}

main,
main [id] { scroll-margin-top: calc(var(--header-height) + var(--space-4)); }

/* A query container, so a block inside can lay itself out against the width it has really been
   given rather than against the width of the window. The transcript is the one that asks. */
main {
  padding-top: var(--space-8);
  container-type: inline-size;
}

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

h1, h2, h3 {
  line-height: 1.14;
  letter-spacing: -0.022em;
  text-wrap: balance;
}

/* Space belongs to what follows, not to what came before: a heading sits close to its own text
   and far from the section above it. */
h1 { font-size: var(--text-h1); margin: 0 0 var(--space-4); }
h2 { font-size: var(--text-xl); margin: var(--space-8) 0 var(--space-4); }
h3 { font-size: var(--text-lg); margin: var(--space-6) 0 var(--space-3); letter-spacing: -0.018em; }

/* A rule above every top-level section heading, so sections are told apart by more than a gap.
   Scoped to headings the page itself wrote: the footer has an h2 of its own. */
main > h2 {
  padding-top: var(--space-7);
  border-top: 1px solid var(--border-muted);
}

main > h2:first-child { padding-top: 0; border-top: 0; }

p, ul, ol, dl, table, pre, blockquote, figure { margin: 0 0 var(--space-4); }

p { text-wrap: pretty; }

/* Nothing caps a paragraph here. What was on this line held every top-level block of prose on both
   sites to `--measure`, about seventy characters, and it is what put a 592px column of text under a
   1024px heading on every page of the subdomain and inside every section of the landing page. The
   King has ruled: "I dont think any section's width should differ, all will be same." A paragraph
   takes the column, the same as the heading above it and the grid below it. */

.lede {
  font-size: var(--text-md);
  line-height: 1.45;
  color: var(--fg-muted);
  letter-spacing: -0.01em;
}

/* A small mono label. It names the thing without competing with it - grey, not teal, because a
   label is not an action.
 *
 * Two of them, and the difference is whether the label is content. `.eyebrow` is a paragraph, so a
 * kicker above a heading cannot pretend to be one and the heading order stays honest; it carries no
 * text on the landing page today - the tagline that sat there was repeated word for word by the
 * headline under it - and it is kept for the next thing that needs a kicker. `.strip-label` is a
 * real h3, because each strip is a list and a list needs a name: the row is the only place a reader
 * or a screen reader is told which of the two rows they have got. */
.eyebrow, .strip-label {
  margin: 0 0 var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* A link is told from its text by an underline, not by a colour, so it stays a link for a reader
   who cannot see the difference between teal and near-black. The teal arrives on hover and on
   focus, where it means "this one". */
a {
  color: inherit;
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--control-border);
  text-underline-offset: 0.18em;
  transition: color var(--speed) var(--ease), text-decoration-color var(--speed) var(--ease);
}

@supports (color: color-mix(in oklab, red 10%, white)) {
  a { text-decoration-color: color-mix(in oklab, var(--accent) 55%, transparent); }
}

a:hover {
  color: var(--accent);
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

ul, ol { padding-left: 1.35rem; }
li { margin-bottom: var(--space-2); }
li:last-child { margin-bottom: 0; }

dt { font-weight: 600; margin-top: var(--space-4); }
dt:first-child { margin-top: 0; }
dd { margin: var(--space-1) 0 0; }

em { font-style: italic; }

code, pre, kbd, samp {
  font-family: var(--font-mono);
  font-size: 0.875em;
  font-variant-numeric: tabular-nums;
}

code {
  background: var(--neutral-muted);
  padding: 0.14em 0.36em;
  border-radius: 3px;
}

pre {
  background: var(--canvas-inset);
  border: 1px solid var(--border-muted);
  padding: var(--space-4) var(--space-5);
  border-radius: var(--radius);
  overflow-x: auto;
  line-height: 1.55;
  tab-size: 4;
}

pre code { background: none; padding: 0; font-size: 1em; }

blockquote {
  border-left: 2px solid var(--border);
  margin-left: 0;
  padding-left: var(--space-4);
  color: var(--fg-muted);
}

.table-scroll { overflow-x: auto; margin-bottom: var(--space-4); }

table {
  border-collapse: collapse;
  width: 100%;
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

th, td {
  text-align: left;
  padding: var(--space-2) var(--space-4) var(--space-2) 0;
  border-bottom: 1px solid var(--border-muted);
  vertical-align: top;
}

th { font-weight: 600; }

img { max-width: 100%; height: auto; }

hr { border: 0; border-top: 1px solid var(--border-muted); margin: var(--space-7) 0; }

/* The address, where it can be selected and copied. A mailto link only works on a machine with
   a mail client configured; on one without, the link silently does nothing, and this is what
   the reader falls back to. */
.address {
  font-family: var(--font-mono);
  font-size: 0.9em;
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------- hero, on every site

   `.hero` is shared. Every front page opens the same way - a mark at the top where there is one,
   a display heading, a standfirst and a row of actions - and they should, because they are one
   site: a reader who follows a products tile from emgeelabs.in to kingshand.emgeelabs.in has not
   gone somewhere else. What differs is what fills each slot. The apex draws its wordmark in CSS
   from an attribute; kingshand's wordmark is a drawing, mirrored out of the tool's own repository,
   and it sits in exactly that slot. A site with no mark to put there opens on the heading instead,
   which is what the restaurant-system subdomain does while its product name is unsettled: the slot
   is whatever the page fragment puts first, so a page with no mark writes none. */

/* No bottom padding, on purpose. The next section's own top margin is the space between one
   section's last line and the next section's rule - a whole space-8. Padding here as well made the
   hero the one block on the page followed by two stacked gaps: twelve pixels under the buttons,
   forty-eight of hero padding, then the next section's sixty-four, which is a hundred and twenty-four
   pixels of blank canvas before the first rule where every other section has sixty-four. The hero
   now ends where its content ends and the section below supplies the gap, so the rhythm above the
   first rule is the rhythm between every pair of sections.
 *
 * Vertical only: the hero is a `.section` and takes the column from it. */
.hero { padding-top: var(--space-6); }

/* The subdomain's hero image: the banner on line 1 of emgee-labs/kingshand's README, mirrored at
   build time and served from this origin.
 *
 * It has no width of its own and must not have one. The drawing is 1200px across and the content
 * column is 1024 at its widest, so `img { max-width: 100% }` above - the one width rule in this
 * file that is not the layout primitive, and the one that stops a wide image widening its column -
 * is what sizes it, and the column decides where its edges are like everything else.
 *
 * The <img> carries the SVG's own width and height, so the box is the right shape before the file
 * arrives and the heading below it does not move when it does. The hairline is for the dimmed
 * theme: the banner's own background is a near-black gradient a few points darker than the canvas,
 * and without an edge it reads as a hole rather than as a picture. */
.hero-banner { margin: 0 0 var(--space-6); }

.hero-banner img {
  display: block;
  border: 1px solid var(--border-muted);
  border-radius: var(--radius-lg);
}

/* The wordmark, in the hero and at the top of the page, which is where a studio's name belongs on
   its own front page. The header carries it everywhere else - including here, once the reader has
   scrolled a little, which is the rule at the foot of this file.
 *
 * It is decoration and it is marked so. The name is announced once, by the header's wordmark: that
 * is the conventional home link, it is what a browser with no scroll-driven animation shows at every
 * scroll position, so it is the real one and this is the copy. A screen reader meeting the same
 * company name twice within sixty pixels is reading furniture.
 *
 * Branding, not a second headline. The typed h1 under it runs to 56px and this is 22-28, so the hero
 * still has one biggest thing in it. The bar needs no rule here - `.brand-mark` is sized in em, so
 * it keeps its proportion to whatever the wordmark is set at. */
.hero-brand {
  display: flex;
  align-items: baseline;
  gap: 0.4em;
  margin: 0 0 var(--space-5);
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: -0.022em;
}

/* The name itself, drawn from the attribute rather than written in the markup, so the served text
   names the company once - in the header, where the real wordmark is. */
.hero-brand::after { content: attr(data-name); }

/* Less air under this h1 than under an ordinary one, which is the opposite of what a display
   heading usually wants and is the right way round here: the box is two reserved lines tall
   whatever is typed into it, so for a good part of every cycle the second line is empty and that
   empty line is already sixty-four pixels of gap the reader can see. The reservation is what stops
   the standfirst moving while the text types and it stays; the margin on top of it is what came
   out. */
.hero h1 {
  font-size: var(--text-display);
  line-height: 1.04;
  letter-spacing: -0.03em;
  margin-bottom: var(--space-3);
}

/* Nothing follows the buttons inside the hero - the microcopy that used to sit under them was
   withdrawn - so their bottom margin was spacing against nothing. */
.hero .actions { margin-bottom: 0; }

/* ------------------------------------------------------ the typed headline

   A cursor typing the three things EmGee Labs does, and backspacing each one:

       We build software for you.
       We build software with you.
       We find the talent for your team.

   The wordmark is already a teal cursor bar, so a cursor writing the headline beside it is the mark
   doing the talking rather than a borrowed effect. That is also why the whole block is set in the
   mono face: a terminal is the thing being drawn, and it is the only way the arithmetic below can
   be exact.

   The preposition is the device. `for`, `with`, `for` is the only word that differs between the
   first two clauses, so it is the only word in the accent: the eye is sent to the word carrying the
   difference. It is a pseudo-element rather than a span, so it is a colour and not an element -
   nothing extra in the markup, and nothing extra to announce.

   What the markup holds is one sentence - `.headline-plain` - and that is the h1's whole text
   content: its accessible name, what a crawler and an agent read, and what is shown wherever this
   block is not. Everything animated is empty markup, with the fragments in `data-word` and
   `data-rest` attributes and drawn by `content` below, which is the reason the served text is never
   the three clauses run together. The parent is aria-hidden as well, so nothing here reaches the
   accessibility tree.

   It is behind `screen and (prefers-reduced-motion: no-preference)`, and that condition does four
   jobs at once: animated text is off for a reader who has asked for less motion, off on paper, off
   in any browser that does not understand the query, and off with no stylesheet at all. In every
   one of those the plain sentence is simply what the h1 says. Nothing hides it back.

   Nothing may reflow, which is the other rule this has to keep, and it is what decides the shape.

   A slot has to be as wide as the longest thing that will ever be typed into it, or the line moves
   on every cycle. The longest whole clause is thirty-three characters, and thirty-three monospaced
   characters do not fit a 390px screen - let alone the 320px one this is also measured at - at any
   size worth calling a headline, while a typed reveal is an animated width and an animated width
   cannot wrap. So each clause is split at a fixed point into two lines, and it is always the same
   two lines at every width: eighteen characters, then fourteen. Both slots are sized to those
   maxima rather than to whichever clause is showing, so a shorter fragment leaves trailing space at
   the end of a line where nobody can see it, instead of a hole in the middle of the sentence.

   The split earns something the last shape did not. The first two clauses share their whole first
   line, so `We build software` is one element typed once, which then stands still while the second
   line changes from `for you.` to `with you.` - between those two clauses that is the only text
   that moves at all. Their shared `you.` cannot be held the same way: a fixed slot for a
   three-or-four letter preposition would leave a visible hole before it, and an auto-width one
   would shove `you.` sideways the moment `with` arrived. So the second line is typed as one piece.

   That makes the round uneven, so it is written out rather than derived from one keyframe and three
   delays. Thirteen and a half seconds, in percent of the round:

     0-8     the first line types                 58-62   the shared first line is backspaced
     8       it hands the cursor down a line      62      the third clause's line takes the cursor
     8-33    clause one's second line             62-71   it types
     33      clause two's second line takes the   71      it hands the cursor down
             cursor the instant clause one's      71-96   clause three's second line
             is clear                             96-100  the third first line is backspaced
     33-58   clause two's second line             100     the round wraps and the first line types

   Nothing is held at the empty state, and that is deliberate: the King asked for it - "when
   backspacing is done, there should not be a pause. I dont think we need to hold there, start typing
   as soon as clearing is done." So the moment a line is clear the next thing starts, and the round
   lost four dwells doing nothing: 8% of it between clause one and clause two with the second line
   blank, two beats before each first-line deletion, and one at column zero before the wrap. What is
   held is the full state - 17% of the round per clause, with the whole sentence on screen - because
   a reader has to be able to read the clause before it is taken away.

   Each second line is 5% typing, 17% held, 3% cleared. The first lines are 8% and 9% typing and 4%
   cleared: deleting runs at about 30ms a character against 65ms to type, because nobody reads text
   as it disappears. The round was 24s and is 13.5s, and no phase became unreadable to get there.

   The three second lines share one keyframe set and differ only by delay, because their three
   phases are evenly spaced; the two first lines have one each. The cursor belongs to exactly one
   element at every instant: each handover is a pair of keyframes four hundredths of a percent apart
   at the same moment on both sides, so the intervals partition the round with no gap and no overlap.

   The size is a min() of three terms and no clamp: the design size, a fluid term, and a share of the
   viewport that keeps eighteen characters inside the gutters however narrow the screen gets.

   Each slot is a one-row grid and every fragment sits in the same cell, so they stack with no
   absolute positioning and the row is one line tall whatever is in it. The cursor is a fragment's
   own right border, so it sits where the typing has reached rather than at a fixed point, and it is
   handed between the two lines the way a terminal hands it over at a wrap. `content-box` matters:
   under the global border-box the border would eat the last letter. */

.headline-type {
  display: none;
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: min(3.5rem, 1.1rem + 3.2vw, 8vw);
  line-height: 1.15;
  /* The sans tracking above would put the cursor a fraction of a letter past the last one: a width
     counted in ch knows nothing about letter-spacing. */
  letter-spacing: normal;
}

.headline-slot { display: grid; }
.headline-slot > * { grid-row: 1; grid-column: 1; justify-self: start; }

/* The longest fragment each line will ever hold: `We find the talent`, then `for your team.`. */
.headline-slot-a { grid-template-columns: 18ch; }
.headline-slot-b { grid-template-columns: 14ch; }

.headline-part {
  /* The cursor's thickness, and the width this fragment is typed out to. Both are stated once
     because every keyframe below spends them. */
  --caret: 0.09em;
  --typed: calc(var(--n) * 1ch);
  box-sizing: content-box;
  overflow: hidden;
  width: 0;
  /* `pre`, not `nowrap`: the second line is a teal preposition and a remainder that begins with a
     space, and that space is the one between two words of the headline. */
  white-space: pre;
  border-right: 0 solid var(--accent);
  animation-name: headline-line-a, headline-cursor;
  animation-duration: 13.5s, 1.1s;
  animation-iteration-count: infinite;
  /* The steps are the fragment's own letters, so the reveal lands between characters and never
     inside one. It is the element's own timing function because the count is the element's own. */
  animation-timing-function: steps(var(--n)), steps(1);
}

/* The first line's two states: the one the first two clauses share, and the third clause's. */
.headline-slot-a > :last-child { animation-name: headline-line-a-late, headline-cursor; }

/* The second line, three times over, at 8%, 33% and 71% of the round. Two of those are the instant
   the line above finishes typing; the middle one is the instant clause one's second line is clear,
   which is what a reader sees as the next clause starting without a pause. */
.headline-slot-b > * { animation-name: headline-line-b, headline-cursor; }
.headline-slot-b > :nth-child(1) { animation-delay: 1.08s, 0s; }
.headline-slot-b > :nth-child(2) { animation-delay: 4.455s, 0s; }
.headline-slot-b > :nth-child(3) { animation-delay: 9.585s, 0s; }

/* The fragment, in two pieces so one of them can be teal. `data-rest` is absent on the first line's
   fragments, and attr() of an absent attribute is the empty string, so that ::after draws nothing. */
.headline-part::before { content: attr(data-word); }
.headline-part::after { content: attr(data-rest); }
.headline-slot-b .headline-part::before { color: var(--accent); }

/* The first line the first two clauses share. It is typed, holds through both of them, and is only
   backspaced when the third clause needs the line - so its cursor leaves once, at 8%, and comes back
   once, at 58%, to delete it. It used to take the cursor back between the two clauses as well and
   hold it there for 8% of the round: that was the pause at the empty state, and it is gone.
 *
 * Every keyframe states both properties, and the cursor is switched across a pair of them four
 * hundredths of a percent apart. That is the whole reason for the odd numbers. `steps()` divides
 * every interval it spans into that many equal jumps, so a border width put at one end of a long
 * interval and zero at the other does not hold and then switch - it shrinks by eighteenths, and two
 * lines both show a sliver of cursor for seconds at a time. Confined to five milliseconds the same
 * eighteen steps are five milliseconds of sliver instead of five seconds.
 *
 * Five milliseconds is still a frame's worth of two cursors every few rounds, so each keyframe that
 * opens one of those windows carries its own `steps(1)`: a keyframe's timing function belongs to the
 * interval that starts at it, and one step means the value it states holds across the whole window
 * and changes at the far end. Both sides of a handover do that, so the outgoing cursor is whole
 * until the instant the incoming one appears. Measured at one sample per millisecond through the
 * round: one cursor at every one of the 13,501 instants, where before this line there were fifteen
 * with two. */
@keyframes headline-line-a {
  0% { width: 0; border-right-width: var(--caret); }
  7.96% { width: var(--typed); border-right-width: var(--caret); animation-timing-function: steps(1); }
  8% { width: var(--typed); border-right-width: 0; }
  57.96% { width: var(--typed); border-right-width: 0; animation-timing-function: steps(1); }
  58% { width: var(--typed); border-right-width: var(--caret); }
  61.96% { width: 0; border-right-width: var(--caret); animation-timing-function: steps(1); }
  62% { width: 0; border-right-width: 0; }
  100% { width: 0; border-right-width: 0; }
}

/* The third clause's first line. It waits out most of the round, takes the cursor the instant the
   shared line above is clear and starts typing on the same instant, and it hands the cursor back at
   100% - where the round wraps and the shared line begins typing again with no dwell between the
   two. It is the only fragment whose deletion ends at the end of the round, which is why its last
   pair is 99.96/100 rather than a hold at column zero. */
@keyframes headline-line-a-late {
  0% { width: 0; border-right-width: 0; }
  61.96% { width: 0; border-right-width: 0; animation-timing-function: steps(1); }
  62% { width: 0; border-right-width: var(--caret); }
  70.96% { width: var(--typed); border-right-width: var(--caret); animation-timing-function: steps(1); }
  71% { width: var(--typed); border-right-width: 0; }
  95.96% { width: var(--typed); border-right-width: 0; animation-timing-function: steps(1); }
  96% { width: var(--typed); border-right-width: var(--caret); }
  99.96% { width: 0; border-right-width: var(--caret); animation-timing-function: steps(1); }
  100% { width: 0; border-right-width: 0; }
}

/* The second line, once: typed to 5%, held to 22%, cleared by 25%, and the cursor handed on at 25%
   - to the line above when that line is next to be deleted, or straight to the next clause's second
   line when it is not. It takes the cursor at the moment the line above finishes, which is what a
   terminal does at a wrap. Its phase is a delay, so all three clauses read this one set. */
@keyframes headline-line-b {
  0% { width: 0; border-right-width: var(--caret); }
  5% { width: var(--typed); border-right-width: var(--caret); }
  22% { width: var(--typed); border-right-width: var(--caret); }
  24.96% { width: 0; border-right-width: var(--caret); animation-timing-function: steps(1); }
  25% { width: 0; border-right-width: 0; }
  100% { width: 0; border-right-width: 0; }
}

/* A terminal rate: on for a little over half a second, off for the same. This animates the border's
   colour while the tracks above animate its width, so the two never contend for one property. */
@keyframes headline-cursor {
  0% { border-right-color: var(--accent); }
  50% { border-right-color: transparent; }
  100% { border-right-color: transparent; }
}

/* The sentence, wrapped to something a display size can hold. This is the visible headline until
   the block above is switched on, and it never stops being the h1's text. */
.headline-plain { display: block; max-width: 24ch; }

@media screen and (prefers-reduced-motion: no-preference) {
  .headline-type { display: block; }

  /* Taken out of sight, never out of the document: the accessible name and the crawled text are
     this element. `display: none` here would leave the h1 with no text at all. */
  .headline-plain {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    border: 0;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* The 44ch cap that was here was the fifth instance of the same defect, and the quietest: at the
   standfirst size on a wide monitor it held the tagline to 546px of a 1024px column. The line is
   thirty-five characters and does not wrap either way, so nothing about it looks different - it was
   simply another element deciding its own width. */
.hero .lede { margin-bottom: 0; }

/* One row of actions, wrapping rather than shrinking. The first is the only thing this page is
   really asking anyone to do, and it is the only filled button on the site. */
.actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
  margin: var(--space-6) 0 var(--space-3);
  list-style: none;
  padding-left: 0;
}

.actions li { margin: 0; }

.button {
  display: inline-block;
  padding: 0.7em 1.25em;
  border: 1px solid var(--control-border);
  border-radius: var(--radius);
  background: var(--canvas);
  box-shadow: var(--shadow-small);
  color: var(--fg);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  transition: border-color var(--speed) var(--ease), background-color var(--speed) var(--ease),
    color var(--speed) var(--ease), transform var(--speed) var(--ease);
}

.button:hover { border-color: var(--fg-muted); color: var(--fg); transform: translateY(-1px); }
.button:active { transform: none; }

.button-primary {
  background: var(--accent);
  border-color: transparent;
  color: var(--fg-on-accent);
}

.button-primary:hover {
  background: var(--accent);
  border-color: transparent;
  color: var(--fg-on-accent);
}

/* The mix is the enhancement: hover moves the fill toward the text colour, so it reads as more
   contrast in either theme - a darker teal in light, a lighter one in the dimmed theme, where
   --fg is near-white. The flat accent is what hover means without it. Left to the cascade this
   would compute to `transparent` instead, and the label would be printed on the bare canvas - a
   button that disappears under the pointer. */
@supports (color: color-mix(in oklab, red 10%, white)) {
  .button-primary:hover { background: color-mix(in oklab, var(--accent) 88%, var(--fg)); }
}

/* Six one-off width rules stood here, and every one of them was an element being let back out of a
   cap this file had just put on it: prose held to the measure everywhere inside a section, then the
   actions, the products grid, the model tiles and the strip tracks let out again by name, then the
   sentence under each heading let out again, then the FAQ answers, then the closing line of the
   contact section. Each was a real fix for a real complaint. Together they were a list of exceptions
   as long as the rule, which is what a rule looks like when it is the wrong rule. The cap is gone,
   so every one of the exceptions went with it and a seventh element cannot need a seventh.

   ------------------------------------------------------ the moving strips

   Two rows under the hero: the work EmGee Labs takes on, and what it is built in. They carry the
   labels a grid of nine described cards used to carry, so the section has one distinction to make
   quickly - what is done, against what it is used - or a client scanning it learns nothing from
   either row. Four things draw that line: the reading face against the mono face, a reading size
   against a label size, the foreground against the muted grey, and opposite directions of travel.
   Each row also has its own heading, which is what names the list for a screen reader.

   How the loop works, and why there is no seam. Each row holds its list twice. Both copies are flex
   items of one clipping box, laid out side by side at their own width, and both are translated left
   by exactly one hundred per cent - their own width. At the end of the round the second copy stands
   precisely where the first stood at the start, so the picture either side of the wrap is the same
   picture and there is nothing for a jump to jump over. The gap after the last item belongs to the
   track as trailing padding rather than sitting between the two tracks: a gap between them would be
   distance the translation does not cover, and that is exactly the visible seam this shape exists
   to avoid. The second row is the same keyframe played in reverse, so both directions are one
   keyframe.

   The two rows are given different durations rather than one, because they are different lengths.
   Equal durations would move the longer row half again as fast as the shorter, and speed is what a
   reader notices first; these two are set to travel at about the same rate, slowly - a round is
   measured in tens of seconds.

   Two things stop it. `:hover` and `:focus-within` pause both copies, so a reader can hold a row
   still to read an item, and the clipping box carries a tab stop so that is reachable from the
   keyboard and not only under the pointer. Pausing is `animation-play-state`, so the row holds where
   it is rather than snapping back to the start.

   All of it is inside `screen and (prefers-reduced-motion: no-preference)`. Outside it - a reader
   who has asked for less motion, paper, a browser that does not know the query, no stylesheet at
   all - the duplicate is simply not displayed and each row is a wrapped list of its own items at the
   width the page has. That is a static readable layout rather than a marquee frozen with half an
   item cut off, and it needs no rule of its own: it is what this markup does with the animation
   absent. The fade below sits in the same query for the same reason: a static wrapped list has no
   edge to soften.

   Where the rows end, which is the one thing here that has now been settled three ways.

   Held to the container with a hard clip, each row cut a word in half against nothing - `ustom
   software` at one edge, `Cloud a` at the other - and that reads as broken text rather than as
   motion. Run to the screen edges instead, the cut landed where a word leaving reads as motion and
   nothing had to fade; that is what pass 6 did, and the King's answer was "this is taking full width
   when other parts taking less" - at 1854px the rows spanned the whole window while every other
   element on the page sat in a 1024px column, so the section read as belonging to a different page.

   Both objections are right, so the rows are back in the container and the clip is a fade. That
   trade was refused twice before, for two reasons, and both have been answered:

   - it clipped the element's own focus ring away. It does not now, because the mask is not on the
     element that carries the ring. `.marquee` is the tab stop and the clipping box; the mask is on
     `.marquee-lane` inside it, so the ring is drawn on an unmasked box at the ordinary +3px offset,
     and there is nothing at the screen edge for it to be painted off any more.
   - a word is below AA while it fades. That one is a real trade and the King has taken it: every
     word crosses the full-contrast middle of the row for almost all of its travel, and a word
     entering or leaving has either not been read yet or has already been read. The alternative is a
     hard cut through a letter, which they rejected, or full bleed, which they have just rejected.

   A mask rather than a gradient in the canvas colour laid over the text. The two are the same
   arithmetic - covering a letter with N% of the backdrop and painting it at (100-N)% opacity over
   that backdrop give the identical pixel - so the only real difference is what the covering colour
   has to be, and here it cannot be the flat canvas: the fixed wash, and on a wide screen a pool,
   both pass behind these rows, so an overlay would have to repaint whatever is under it to stay
   invisible. A mask has nothing to match. `mask-repeat: no-repeat` matters - the initial value tiles
   the gradient, which would put a second fade beyond each edge.

   How wide the fade is, which pass 9 had to answer twice. It was `--space-6`, 32px, chosen to keep
   the interval where a letter is below AA as short as possible - and at the capabilities row's type
   size 32px is about two characters, which in a render still reads as a hard cut: the left edge
   showed `ustom software` with the C sliced off and the right showed `Cloud a`. A fade that does not
   dissolve a word is the cut it was meant to replace, so it is wider.

   `min(13%, 7rem)`. 112px wherever the row is wide enough to spend it, which is a window of about
   910px and up, and a proportion of the row below that. It has to scale: at 320px the content column
   is 296px and the longest item on the work row is over 200 of them, so a fixed 112px at each end
   would leave a middle no whole item fits in - the fade would be eating the content rather than its
   edges. Thirteen per cent of the row is 38px at 320px and 47px at 390px, which is four to five
   characters, and it is measured against the row itself rather than against the window, so a
   scrollbar changes it correctly and no media query has to know about it.

   The cost is the trade the King took in pass 7: a letter inside the fade is below AA while it is
   entering or leaving, and a wider fade means more pixels in that state. At the rate these rows
   travel - about 62px a second on the work row and 65 on the technologies - a word now spends about
   1.8 seconds fading in and 1.8 fading out of a crossing that takes some sixteen. The labels stay
   where they are: they are content, and they keep the left edge every other heading on this page
   shares.

   Nothing here needs the overflow machinery the full-bleed version did. `overflow-x: auto` on the
   root and `overflow-x: clip` on the landing body existed to absorb the half a scrollbar `100vw` is
   wider than the page; inside the container there is nothing to overflow, so both are gone - and
   with them the `:has()` scoping, and the eight pixels of sideways scroll a browser without `:has()`
   was getting on this one page.

   One thing the box still needs. Each copy has to be at least as wide as the box, or the translation
   runs out of track and the wrap shows a hole - which is the same seam in a new place. `min-width:
   100%` states it, and `space-around` spends the width that adds on the gaps rather than leaving it
   in one lump at the end of the track. Neither is binding at the container's width today, because
   both lists are longer than 1024px; they are what stops a shortened list from putting the hole
   back. */

.strip-row { margin-top: var(--space-5); }
.strip-row + .strip-row { margin-top: var(--space-6); }

/* The clipping box. An element's own outline is not clipped by its own overflow, so the focus ring
   the tab stop needs is drawn whole even though everything inside is cut off at the edges. */
.marquee { overflow: hidden; }

.marquee-track {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-7);
  margin: 0;
  padding: 0;
  list-style: none;
}

.marquee-track li { margin: 0; }

/* The duplicate. Off unless the animation is running, which is why paper and reduced motion need no
   rule to take it away again. */
.marquee-copy { display: none; }

.marquee-copy li::before { content: attr(data-name); }

/* What tells the two rows apart: the work is read, so it is set as prose; the technologies are
   labels, so they are set as labels. */
.strip-work li {
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--fg);
}

.strip-tech li {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--fg-muted);
}

/* The motion, behind the one thing it cannot do without.
 *
 * The rows travel and the ends dissolve, and those are not two features: without the fade the
 * travelling row is `.marquee`'s own `overflow: hidden` cutting a word through a letter - `ustom
 * software` at one edge, `Cloud a` at the other - which is the state the King rejected and the state
 * the fade was written to replace. So the whole of the motion waits on the mask, the way the dot
 * field at the top of this file waits on it: an engine that cannot draw the fade gets no animation
 * and no duplicate, which is the static wrapped list this block's contract already names as the
 * fallback, rather than the cut.
 *
 * `-webkit-` beside the unprefixed pair for the same reason, one step earlier: unprefixed masking is
 * Chromium 120 and Safari 15.4, and before those only the prefixed properties were recognised. Those
 * engines can fade, so they should - and the feature query asks for either spelling. */
@media screen and (prefers-reduced-motion: no-preference) {
  @supports (mask-image: linear-gradient(#000, transparent)) or
            (-webkit-mask-image: linear-gradient(#000, transparent)) {
    /* The lane holds both copies side by side and carries the fade. It is a box inside `.marquee`
       rather than `.marquee` itself for one reason: the mask would otherwise take the two ends of
       the focus ring with it, and `.marquee` is the tab stop. `no-repeat` because the initial value
       tiles the gradient, which draws a second fade in from each edge of the box. */
    .marquee-lane {
      /* A proportion of the row with a ceiling, not a fixed number: 112px once the row can spare it,
         and 13% of the row when it cannot. Measured against the lane rather than the window, so it
         is right whether or not a scrollbar is drawn. */
      --strip-fade: min(13%, 7rem);
      display: flex;
      -webkit-mask-image: linear-gradient(to right,
        transparent, #000 var(--strip-fade), #000 calc(100% - var(--strip-fade)), transparent);
      mask-image: linear-gradient(to right,
        transparent, #000 var(--strip-fade), #000 calc(100% - var(--strip-fade)), transparent);
      -webkit-mask-repeat: no-repeat;
      mask-repeat: no-repeat;
    }

    .marquee-track {
      flex: none;
      flex-wrap: nowrap;
      /* At least as wide as the box it travels through, or the wrap shows a hole - and the width
         that adds is spent on the gaps, which keeps the join between the two copies one gap wide. */
      min-width: 100%;
      justify-content: space-around;
      /* The trailing gap is the track's own, so translating the track by its own width covers it. */
      padding-inline-end: var(--space-7);
      animation: marquee-track 60s linear infinite;
    }

    .strip-work { animation-duration: 68s; }
    .strip-tech { animation-duration: 46s; }

    .marquee-copy { display: flex; }

    .marquee-reverse .marquee-track { animation-direction: reverse; }

    .marquee:hover .marquee-track,
    .marquee:focus-within .marquee-track { animation-play-state: paused; }
  }
}

@keyframes marquee-track {
  from { transform: translateX(0); }
  to { transform: translateX(-100%); }
}

/* ------------------------------------------------------ showcase

   What has been built, one tile per thing, and the centre of the landing page. There are two tiles
   today, and the grid is written for a list rather than for a fixed count: the second one dropped
   into the row beside the first with nothing here changing, and a third will do the same.

   The tiles share the row, and that is the `.columns` primitive rather than a grid written here:
   one product fills the row, two take half each, three take a third, and the fourth to arrive wraps
   to a row below. It takes the primitive's own 15rem collapse point, so this row sets nothing at
   all about its width. What it keeps is the vertical space above it and the list reset.

   The whole tile is the link, not the word inside it, so the pointer target and the focus ring are
   the tile itself. Quiet by construction: a tinted panel and a hairline, no shadow and no gradient.

   What is in it: the name, one line saying what the thing is, and an arrow. The name was all there
   was for a pass, and one word centred in a panel a thousand pixels wide and a hundred and thirty
   tall is a box with nothing in it - the King's word was garbage, and they were right. A stranger
   has to be able to decide whether to click without following the link to find out what it is, so
   the line under the name is the tile's real content. The arrow is the affordance: the tile is a
   link, and a panel does not look like one.

   Left-aligned, and no minimum height. Centring was correct for a single word and is wrong for two
   lines - centred, the name and the line under it share no edge with each other or with anything
   else on the page. The height is now whatever the content needs; `height: 100%` is what keeps two
   tiles in one row the same height as each other rather than a fixed tall box keeping them both
   taller than either needs. */

.showcase {
  margin-block: var(--space-6) 0;
  padding-left: 0;
  list-style: none;
}

.showcase li { margin: 0; }

.showcase a {
  display: grid;
  grid-template-columns: 1fr auto;
  align-content: start;
  gap: var(--space-2) var(--space-4);
  height: 100%;
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--canvas-subtle);
  color: var(--fg);
  text-decoration: none;
  transition: border-color var(--speed) var(--ease), color var(--speed) var(--ease);
}

/* The name slot is set in the label face - the same one the strip's technologies are in - and it is
   the only line of the tile at a reading size. It holds a product name where there is one, and a
   description where there is not: the second tile's product has no settled name, so its slot says
   what the thing is instead. Both read as the tile's own label, which is what this face is for. */
.showcase-name {
  font-family: var(--font-mono);
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* One line, and it stays one thing to read: it spans both columns so it runs under the arrow
   rather than being squeezed into the column beside it. */
.showcase-note {
  grid-column: 1 / -1;
  color: var(--fg-muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* Decoration for the link, drawn here rather than fetched, and it takes the tile's own colour - so
   hover moves the arrow and the name together and the line under them stays quiet. */
.showcase-cue {
  grid-column: 2;
  grid-row: 1;
  align-self: center;
  justify-self: end;
  color: var(--fg-muted);
}

.showcase a:hover { border-color: var(--accent); color: var(--accent); }
.showcase a:hover .showcase-cue { color: var(--accent); }

/* ------------------------------------------------------ engagement models

   A choice a reader makes, and there are three of them, so they are tiles: a panel each, an icon,
   the name, the description. Three is few enough to be looked at side by side and compared, which
   is what somebody deciding how to buy the work is actually doing. Same panel as the showcase - a
   tinted surface and a hairline, no shadow and no gradient - because a tile on this page already
   means one thing and it should not start meaning two.

   The row is the `.columns` primitive, at its own 15rem, which is the same row the products grid
   is - and that is the point: two rows of tiles that look alike now are alike, rather than being two
   copies of one declaration that a later edit can move apart. */

.models {
  margin-block: var(--space-6) 0;
  padding-left: 0;
  list-style: none;
}

/* A tile's heading is a label, so it is the weight and not the size that tells it from the prose
   under it. At --text-lg these would compete with the section heading three times over. */
.models h3 {
  margin: 0 0 var(--space-2);
  font-size: var(--text-base);
  line-height: 1.3;
  letter-spacing: -0.012em;
}

.models p {
  margin: 0;
  color: var(--fg-muted);
  font-size: var(--text-sm);
}

.models li {
  margin: 0;
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--canvas-subtle);
}

/* Decoration, and drawn in the accent because that is what the accent is for: the one mark on an
   object that says which object it is. The tile says everything it has to say without it. */
.model-icon {
  display: block;
  margin-bottom: var(--space-4);
  color: var(--accent);
}

/* ------------------------------------------------------ FAQ

   <details> and <summary>: an accordion that opens, closes, takes focus and answers the keyboard
   with nothing running. The one script these sites serve does the theme and nothing else - rule 2
   at the top of this file - so the alternative was not a nicer accordion, it was six headings and
   six paragraphs. Both states of a closed answer are in the served bytes either way, so a crawler
   and an agent read every answer whether or not anything was clicked.

   The default marker is dropped and a sign put on the far side instead. Not decoration: the
   marker is laid out inside the summary's own box, which indents six summaries away from the one
   left edge every other line on the page shares. `details[open]` is the browser's own attribute,
   toggled by the browser, so reading it is not the same as waiting for a class a script adds. */

.faq {
  margin: var(--space-6) 0 0;
  border-top: 1px solid var(--border-muted);
}

.faq details { border-bottom: 1px solid var(--border-muted); }

.faq summary {
  display: flex;
  gap: var(--space-4);
  align-items: baseline;
  justify-content: space-between;
  padding: var(--space-4) 0;
  font-weight: 600;
  cursor: pointer;
}

.faq summary:hover { color: var(--accent); }

/* Safari draws its marker through a pseudo-element rather than through `display`. */
.faq summary::-webkit-details-marker { display: none; }

.faq summary::after {
  content: "+";
  flex: none;
  font-family: var(--font-mono);
  font-weight: 400;
  color: var(--fg-muted);
}

.faq details[open] summary::after { content: "\2212"; }

/* ------------------------------------------------------ contact

   The page's closing call to action, and the destination of both the hero's primary button and the
   header's Contact button. It was a heading, a line, a button and an address stacked in the left
   half of a full-width band with a void to the right of them - the King: "this feels too empty" -
   and what fills it is what happens after a reader writes.

   It was two columns for one pass and that was the wrong answer to the emptiness: it moved the void
   rather than closing it, because four short things and three long steps are two very unequal
   columns. This is one column - the studio line, the invitation, the button, the address - and then
   the three steps as a row of cards under all of it. A card row fills the width by itself, so there
   is no second column whose length has to be argued with.

   The row is the `.columns` primitive, at the primitive's own collapse point, so it decides nothing
   about its width: three cards at desktop, one per row on a phone, in source order either way.

   What the cards must not be is the third row of tiles on this page. The engagement models above are
   a set of parallel things a reader chooses between and are drawn as panels for exactly that reason;
   these are ordered and dependent, and two identical card treatments on one page read as filler. So
   they are the lighter of the two, and every difference is in the same direction:

     - no panel and no border box. A single rule across the top of each card and nothing else, which
       reads as three segments of one sequence rather than three objects to pick from.
     - no icon. The number is the mark, in the accent, in the same place and at the same size the
       model tile's icon takes - which is what the King asked for, and the accent is already what
       this page uses for the one mark that says which object this is.
     - no fill, so the cards sit on the canvas rather than on a tinted surface.

   The number is a counter rather than a marker: `list-style: none` puts the marker's box outside the
   card, and this one has to sit inside it, above the text. The list stays an <ol> so the sequence
   survives into the accessibility tree, llms.txt and the mirrored markdown, and it carries
   `role="list"` because Safari drops list semantics from a list with its markers switched off. */

/* Its heading names the row rather than opening a section, so it takes the weight and not the size
   - the same call the model tiles make, and for the same reason: at --text-lg it would be a third
   heading competing with the h2 above it. */
.steps-heading {
  margin: var(--space-7) 0 var(--space-4);
  font-size: var(--text-base);
  line-height: 1.3;
  letter-spacing: -0.012em;
}

.steps {
  margin: 0;
  padding-left: 0;
  list-style: none;
  counter-reset: step;
  gap: var(--space-5) var(--space-4);
}

.steps li {
  margin: 0;
  padding-top: var(--space-3);
  border-top: 2px solid var(--border);
  counter-increment: step;
  font-size: var(--text-sm);
  line-height: 1.55;
}

/* Where the model tile puts its icon, and drawn in the accent for the same reason it is: the one
   mark on an object that says which object it is. Here it says which one of three, and in what
   order, which is the thing a card row does not otherwise say. */
.steps li::before {
  content: counter(step);
  display: block;
  margin-bottom: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1;
  color: var(--accent);
}

/* ------------------------------------------------------ transcript

   A real run of the tool, printed as it came out, on the tool's own subdomain - the detail of one
   tool belongs there rather than on the company's front page. It is the one object on that page
   meant to be looked at rather than read first, so it is lifted off the canvas as a card and tilted
   just enough to read as a thing on a desk. The tilt is a fraction of a degree - any more and it
   becomes the point. The tilt and the shadow only arrive once <main> is wide enough to carry them,
   which is what the container query below asks. */
.transcript { margin: var(--space-6) 0 0; }

.transcript pre {
  font-size: var(--text-xs);
  padding: var(--space-5);
  margin-bottom: var(--space-4);
  border-radius: var(--radius-lg);
  border-color: var(--border);
  box-shadow: 0 10px 30px -12px rgb(0 0 0 / 0.25);
}

/* Depth is three shadows at different blurs, not one. A single soft shadow reads as a sticker. */
@supports (color: light-dark(#fff, #000)) {
  .transcript pre {
    box-shadow:
      0 1px 2px light-dark(rgb(31 35 40 / 0.06), rgb(1 4 9 / 0.5)),
      0 8px 20px -8px light-dark(rgb(31 35 40 / 0.12), rgb(1 4 9 / 0.55)),
      0 30px 60px -28px light-dark(rgb(31 35 40 / 0.22), rgb(1 4 9 / 0.7));
  }
}

@media (prefers-reduced-motion: no-preference) {
  @container (min-width: 44rem) {
    .transcript pre {
      transform: rotate(-0.45deg);
      transition: transform 260ms var(--ease);
    }
    .transcript pre:hover { transform: rotate(0deg); }
  }
}

.transcript figcaption {
  font-size: var(--text-xs);
  color: var(--fg-muted);
  line-height: 1.55;
}

.transcript .prompt { color: var(--fg-muted); }

/* ------------------------------------------------------ the transcript, typing

   The demo. The King asked for one and said a small video if it were possible, "if no, whatever you
   like" - and a video is a large binary that cannot be reviewed in a diff, on a site whose whole
   argument is that it is text on disk. So the transcript that was already on this page types itself
   out, with the same mechanism the apex headline uses: a monospaced box whose width is animated from
   zero to its own character count in `ch`, in that many steps, so the reveal lands between letters
   and never inside one.

   ------------------------------------------------------ one copy, not two

   The headline needs a second, plain copy of its sentence because its animated fragments are three
   clauses that are never all true at once. This does not, and must not: the transcript is verbatim
   output from a real run, and two copies of evidence is one copy that can be edited without the
   other noticing. So each line's span holds its own real text. The served markup, llms-full.txt, a
   crawler and a screen reader therefore all get the finished fifteen lines and never a fragment of
   one - clipping is a visual state here and nothing more. sites/kingshand/transcript.mjs owns the
   characters and hands each line the two numbers below.

   ------------------------------------------------------ nothing moves while it types

   Three things would move, and each is held:

   - Vertically, a line's box is one line tall at every width from zero characters up, because the
     text does not wrap: `white-space: pre` inside a box that clips. `vertical-align: top` is what
     keeps that box the same height as the line box around it - an inline-block that clips aligns
     its bottom edge to the baseline by default, which would drop every row by the strut's descender
     and make the card taller than the same card standing still.
   - Horizontally, the card would gain a scrollbar partway through the run: nothing overflows while
     the lines are short, and the longest line is wider than the card on a phone. So the code block
     reserves the widest line's width from the first frame, in `ch`, and whether the card scrolls is
     decided before anything is typed rather than during it. One character of that is the caret's
     room: the bar is painted outside the last character and must not be the thing that tips the
     card into scrolling.
   - The caret itself is a right border on the box being typed, so it travels with the text rather
     than sitting at a fixed point, and `content-box` keeps it from eating the last letter.

   ------------------------------------------------------ one caret, and where the run ends

   Each line is given the instant it starts, and the starts are cumulative, so a line's window opens
   after the line above it has closed: `transcript-line` ends by taking the caret away, and the next
   line's own animation is what puts one back. Two are never painted at once, and in the pause
   between two lines there is none at all - which is what a terminal looks like between one result
   and the next.

   The run does not loop. It types once, holds the whole transcript, and leaves the caret blinking
   at the end of the last line, because that is what the machine actually did; a transcript that
   erased itself every thirteen seconds would be an animation rather than a record. The last line
   therefore takes a keyframe set of its own - the only difference is that it keeps the caret - and
   the blink is the only thing on this page still moving a minute after it loads.

   Behind `screen and (prefers-reduced-motion: no-preference)`, which does four jobs: a reader who
   asked for less motion gets the transcript standing still, so does paper, so does a browser that
   does not know the query, and so does a page with no stylesheet at all. Outside it not one
   declaration below applies, so the static state is the card exactly as it was before any of this -
   which is why the reserved width is inside the query too: on paper the card wraps instead of
   scrolling, and a reserved width would make it overflow the sheet. */

@media screen and (prefers-reduced-motion: no-preference) {
  /* The room the card reserves. `--typed-width` is the widest line plus the caret's character,
     written onto the element by sites/kingshand/transcript.mjs - it is a fact about the transcript,
     so the transcript is what states it. A block, because that is what min-width applies to. */
  .transcript code {
    display: block;
    min-width: var(--typed-width);
  }

  .typed-line {
    /* The caret's thickness, and the width this line is typed out to. Both are spent by the
       keyframes below, and `--n` and `--rate` and `--start` arrive on the element itself. */
    --caret: 0.12em;
    --typed: calc(var(--n) * 1ch);
    display: inline-block;
    vertical-align: top;
    box-sizing: content-box;
    overflow: hidden;
    width: 0;
    white-space: pre;
    border-right: 0 solid var(--accent);
    animation-name: transcript-line;
    animation-duration: calc(var(--n) * var(--rate));
    animation-delay: var(--start);
    /* The line's own letters, so a step is a character. */
    animation-timing-function: steps(var(--n));
    /* Forwards and not both: before its delay elapses a line has to be at the width this rule
       gives it, with no caret, because nothing has typed it yet. `both` would paint a caret on
       every line of the run from the first frame. */
    animation-fill-mode: forwards;
  }

  .typed-line-last {
    animation-name: transcript-line-end, transcript-caret;
    animation-duration: calc(var(--n) * var(--rate)), 1.1s;
    animation-delay: var(--start), var(--start);
    animation-timing-function: steps(var(--n)), steps(1);
    animation-iteration-count: 1, infinite;
    animation-fill-mode: forwards, none;
  }
}

/* The pair at 99% and 100% is the handover, and it is why the number is not 100 twice: `steps()`
   divides whatever interval it spans, so a border width put at one end of the typing interval and
   zero at the other would shrink by hundredths for the whole run instead of switching off at the
   end of it. Confined to the last hundredth, with `steps(1)` so the value it states holds across
   that window, the caret is whole until the instant it goes. */
@keyframes transcript-line {
  0% { width: 0; border-right-width: var(--caret); }
  99% { width: var(--typed); border-right-width: var(--caret); animation-timing-function: steps(1); }
  100% { width: var(--typed); border-right-width: 0; }
}

/* The last line, which keeps its caret. */
@keyframes transcript-line-end {
  0% { width: 0; border-right-width: var(--caret); }
  100% { width: var(--typed); border-right-width: var(--caret); }
}

/* A terminal rate. This animates the border's colour while the track above animates its width, so
   the two never contend for one property - the same split the headline's cursor is built on. */
@keyframes transcript-caret {
  0% { border-right-color: var(--accent); }
  50% { border-right-color: transparent; }
  100% { border-right-color: transparent; }
}

/* ------------------------------------------------------ described lists

   Two lists on kingshand are a term and a paragraph about it: what the one session does, and the
   pointers for getting going. That is a `<dl>`, and it is one here rather than six headings so the
   six read as one list of comparable things - which is what they are, and what the King's own table
   of them is. The restaurant system's four - two sets of capabilities, the stack and the limits -
   are that same shape, and they take this class rather than a second set of rules.

   Everything below is a rule between the pairs and vertical space around them. The description
   keeps the ordinary foreground rather than the muted grey a caption would take: several of these
   carry a `<code>` example inside them, and code is drawn on a tinted chip, so muting the text
   would put the one colour combination on this page nobody has measured underneath the one thing
   the section exists to show. Nothing here sets a width - the column is the section's. */
.capabilities, .pointers { margin-top: var(--space-6); }

.capabilities dt, .pointers dt {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-muted);
}

.capabilities dt:first-child, .pointers dt:first-child { margin-top: 0; }

/* The example is the point of a pointer - the King asked for these to replace install steps
   precisely because a pointer without one is not worth reading - so it is the weight that says so. */
.pointers dd code { font-weight: 600; }

/* ------------------------------------------------------ screenshots

   A row of real captures of a running application, each with a caption saying what it is.

   It is `.columns` doing the placement, as every other row on these sites is, and this class sets
   the one property a row is allowed to set - the width a track collapses at - plus what a captured
   screen needs that a diagram does not. 12rem rather than the 15rem default because these are phone
   captures: three of them sit side by side in the column on a laptop and stack on a phone, and a
   wider track would put two on a line with a gap where the third should be.

   The border is the same hairline the mirrored banner on kingshand carries, and for the same
   reason: a screenshot whose own background is near the canvas colour reads as a hole in the page
   rather than as a picture, in one theme or the other. The tinted fill behind it is what occupies
   the reserved box while the file is still arriving.

   Nothing here sets a horizontal margin or padding, and the width it does set is on the image
   inside a track rather than on a section or a section's own child - the same exemption `img` and
   `.hero-banner img` already have, and for the same reason. */
.shots { --column-min: 12rem; margin-top: var(--space-6); }

.shots figure { margin: 0; }

.shots img {
  display: block;
  width: 100%;
  background: var(--canvas-subtle);
  border: 1px solid var(--border-muted);
  border-radius: var(--radius-lg);
}

.shots figcaption {
  margin-top: var(--space-3);
  color: var(--fg-muted);
  font-size: var(--text-sm);
}

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

   A list of links with a note each, which is what most of this site's lists actually are. */
.linked { list-style: none; padding: 0; }
.linked li { margin-bottom: var(--space-3); }
.linked a { font-family: var(--font-mono); font-size: var(--text-sm); }
.linked span { color: var(--fg-muted); }

/* ---------------------------------------------------------------- footer */

footer {
  margin-top: var(--space-9);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border-muted);
  color: var(--fg-muted);
  font-size: var(--text-sm);
}

footer h2 { font-size: var(--text-sm); margin: 0 0 var(--space-2); }

/* Not held to the prose measure, which is the one place on the page where the cap was wrong rather
   than merely arguable. The four link columns under this paragraph span the whole container, so a
   paragraph stopped at seventy characters ends in the middle of the footer and neither of its edges
   lines up with anything - the same complaint the King made about the FAQ answers. It is two lines
   of small grey type, not a passage anyone reads along, so the measure buys nothing to trade. */
footer p { margin-bottom: 0; }

footer ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  list-style: none;
  padding: 0;
  margin: var(--space-4) 0 0;
}

footer li { margin: 0; }

/* Grouped links, where a site has enough of them that one row stops being a list and becomes a
   heap. Four kinds of destination sit down here - a section of this page, an address, a repository,
   a file written for programs - and the group heading says which before the link is read. It is the
   `.columns` primitive at 10rem: the tracks are narrower than a tile's, because a group is a heading
   and three short links rather than a panel, so the groups sit in one row on a monitor and stack on
   a phone. Each group is a heading and a list either way, so the order survives with no
   stylesheet. */
.footer-groups {
  --column-min: 10rem;
  gap: var(--space-5);
  margin-top: var(--space-6);
}

.footer-groups h3 {
  margin: 0 0 var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

.footer-groups ul {
  display: block;
  margin: 0;
}

.footer-groups li { margin-bottom: var(--space-2); }
.footer-groups li:last-child { margin-bottom: 0; }

/* ---------------------------------------------------------------- narrow screens

   The type scale is fluid, so these are the few things that are a layout decision rather than a
   size: the page gutter, and the section rhythm closing up when there is less room to spend. */

@media (max-width: 30rem) {
  /* The gutter narrows, and it narrows in one place: `.page` is the only rule that spends it, so
     this moves the edges of the header, the main column and the footer together and cannot move one
     of them without the others. */
  :root { --gutter: var(--space-4); }
  body { padding-bottom: var(--space-8); }
  main { padding-top: var(--space-7); }
  .hero { padding-top: var(--space-4); }
  .headline-plain { max-width: none; }
  .section + .section { margin-top: var(--space-7); padding-top: var(--space-6); }
  .actions .button { text-align: center; }

  /* The bar is stuck to the top of the window, so on a phone it is not spacing - it is screen. At
     the desktop padding and the desktop gaps it was 114px of a 844px phone, thirteen and a half per
     cent of everything the reader can see, and it was that tall because the navigation had wrapped
     to a second line with twelve pixels to spare. Closing the gaps puts it back on one line and the
     shorter padding takes the rest: 62px, seven per cent, and one row rather than two.
     `--header-height` follows it, or every anchor would reserve room for a bar this size no longer
     has - and since the toggle joined the bar it is a second row again below 30rem, at 98px, so
     what follows it down here is smaller than the tier above rather than as small as it once was. A
     visible control in a header costs a phone some screen; hiding the word below is what keeps that
     to one extra row instead of two. */
  header { padding-block: var(--space-3); gap: var(--space-2) var(--space-3); }
  header nav ul { gap: var(--space-1) var(--space-3); }
  header nav a.button { padding: 0.4em 0.7em; }
  .header-end { gap: var(--space-1) var(--space-3); }

  /* The bar is stuck to the top of a phone, so every pixel of it is screen rather than spacing -
     which is the whole reason the block above exists. The toggle's word is what goes: a switch
     beside a header's navigation is a recognisable thing, and the control keeps its full name for
     anything reading the page aloud, because that name is an attribute rather than this text.
     Taken out of sight and never out of the document, the way the headline's plain sentence is. */
  .theme-toggle { padding: 0.4em 0.55em; gap: 0; }

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

  /* Two rows at every width in this band, and 98px at the tallest of them - measured at 320, 340,
     360, 384, 390, 420 and 460px, where the bar is 97.7 to 98.0 and never one row. The tier below
     30rem used to be 4rem for a single row and there was a second tier under 24rem for the wrap;
     with the toggle in the bar the wrap happens throughout, so one number covers the band and the
     narrower tier had nothing left to say. It rounds up, like every other value here. */
  body.landing { --header-height: 6.25rem; }
}

@media (max-width: 22rem) {
  :root { --gutter: var(--space-3); }
  .showcase a, .models li, .transcript pre { padding: var(--space-3); }
}

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

   Three things, all of them CSS and none of them able to hold up reading:

   - a short reveal as a section scrolls in, driven by the scroll position itself rather than by
     an observer in script. A browser without scroll-driven animation never runs it and sees the
     finished state, which is the whole content.
   - a cross-fade between pages of the same origin, using cross-document view transitions.
     Unsupported browsers navigate instantly, which is what they do today.
   - transitions on the things a pointer touches.

   All three are switched off for a reader who has asked for reduced motion. */

@view-transition { navigation: auto; }

/* A cross-fade, and only a cross-fade: the outgoing page fades out while the incoming one fades in.
   Nothing travels and nothing is clipped.
 *
 * What was here rose the new page over the canvas as a panel - `clip-path: inset(100% 0 0 0)`
 * animating to `inset(0)`. Live it was the better effect; captured it was a full-width horizontal
 * edge crossing the viewport with the outgoing page still painted above it, and it falsified two of
 * the King's screenshots - a FAQ question that appeared twice, and a rule crossing the products tile
 * - each of which cost a pass to rule out as a real defect. A transition that makes the page look
 * broken whenever somebody photographs it is a net loss however good it looks in motion, so it is a
 * fade. Both halves are the same duration and the same easing, which is what makes the two opacities
 * sum to one throughout and the fade even - the browser composites this pair with plus-lighter. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) {
    animation: 220ms var(--ease) both page-out;
  }

  ::view-transition-new(root) {
    animation: 220ms var(--ease) both page-in;
  }

  @keyframes page-out {
    to { opacity: 0; }
  }

  @keyframes page-in {
    from { opacity: 0; }
  }

  @supports (animation-timeline: view()) {
    /* Every section but the first. The hero is already on the screen when the page opens, so a
       reveal on it would be an entry animation on something that never entered. */
    .section + .section {
      animation: rise linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 45%;
    }

    @keyframes rise {
      from { opacity: 0; transform: translateY(14px); }
      to { opacity: 1; transform: none; }
    }
  }
}

/* The header's wordmark, on the landing page only: absent at the top of the page, where the hero
   carries the name at four times the size, and faded in over the first stretch of scroll so it is
   there for the rest of the page. One name on the screen at a time rather than the same name twice,
   sixty pixels apart.
 *
 * Which way round this is written is the whole of it. The wordmark is visible by default and this
 * block is the only thing that hides it, so a browser with no scroll-driven animations - Safari and
 * Firefox today - keeps the header wordmark at every scroll position and simply also sees the hero's.
 * That is today's page: redundant, correct, and never a header with nothing in it. Written the other
 * way round, with the reveal as the thing that makes it visible, those readers would lose the
 * wordmark for good.
 *
 * Three more things it has to be:
 *
 * - opacity, and nothing else. `display: none` or `visibility: hidden` would take the name out of
 *   the accessibility tree at the top of the page, and the hero's copy is marked decorative - so the
 *   company name would be announced nowhere at all. At opacity 0 it is still read, still crawled,
 *   and the header's height never changes, so the page does not jump on the first scroll.
 * - focus brings it back. An element that can take the keyboard while it is invisible is a focus
 *   ring drawn on nothing; cancelling the animation returns it to its base opacity, which is 1.
 * - `screen`, so paper is untouched and needs no override - a printed page has no scroll position
 *   for a scroll timeline to resolve against.
 *
 * `scroll(root)` rather than a view timeline on the hero: the header is not inside the hero, so a
 * view timeline would have to be published through `timeline-scope` on body to be visible here, and
 * the range would then depend on whether the hero is taller than the window. A length off the top of
 * the document is the same distance at every viewport, and it is the distance the hero occupies. */
@media screen and (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    .landing header .brand {
      animation: brand-reveal linear both;
      animation-timeline: scroll(root block);
      animation-range: var(--space-7) 14rem;
    }

    .landing header .brand:focus-within { animation-name: none; }

    @keyframes brand-reveal {
      from { opacity: 0; }
      to { opacity: 1; }
    }
  }
}

/* Paper has no scroll position, so the reveal has nothing to resolve against and would print a
   blank section. Everything that only exists on a screen is switched off here, and the transcript
   wraps instead of scrolling, because a printed page cannot be scrolled sideways. */
@media print {
  body::before { display: none; }

  /* Paper has no window for a bar to stick to, and a sticky box in paged media is either repeated on
     every sheet or left where it was - neither is what a printed page wants. It goes back in flow,
     which is one masthead on the first sheet, and the wash it was repainting is off here anyway. */
  header {
    position: static;
    background-image: none;
  }

  /* The only control any of these sites has, and paper has no controls. It is not content - the
     page says nothing less without it - and printed it would be a word and a small drawn switch
     claiming to do something a sheet of paper cannot. Everything else in the bar prints. */
  .theme-toggle { display: none; }

  .section {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  .transcript pre {
    transform: none;
    box-shadow: none;
    overflow-x: visible;
    white-space: pre-wrap;
    word-break: break-all;
  }

  /* The action rows are the page's real link list, so they print. Flattened to plain text: on
     paper a filled rectangle is ink, not an affordance. */
  .button {
    padding: 0;
    border: 0;
    background: none;
    box-shadow: none;
    transform: none;
    color: inherit;
  }

  /* An answer nobody happened to open is still an answer, and paper has no disclosure widget to
     open it with. The UA hides a closed <details>' content by making it skipped, so the print
     sheet un-skips it - which shows more of the page rather than less, and the plus sign that
     asked for a click stops meaning anything on paper. */
  @supports selector(::details-content) {
    details::details-content { content-visibility: visible; }
    .faq summary::after { content: ""; }
  }
}

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }

  /* Said twice on purpose. The line above turns the transition off at the source, and this one
     stops the snapshots animating even if an engine takes @view-transition only at the top level
     of a stylesheet. Honouring this preference is not something to leave to one rule. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }

  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }

  .button:hover { transform: none; }
}
