/* ══════════════════════════════════════════════════════════════════════════
   Panoptic Bio company page - shared deck base

   Shared by the four direction files so they differ in STRUCTURE, not in
   material. Tokens, type scale, buttons, header and footer live here; each
   direction adds only its own layout in its own <style> block.

   Production inlines all of this into the single self-contained artifact.
   A separate stylesheet exists here purely so four drafts stay comparable.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Font approaches ───────────────────────────────────────────────────────
   Four candidates, switchable live with ?font= so they can be judged on the
   real page rather than in a specimen. Sans only, no serif, no mono anywhere.
   Every one is SIL OFL or equivalent, so every one is self-hostable.
   Production ships exactly ONE, subset to Latin, from public-company/fonts/.

     figtree     Figtree             humanist warmth              (default)
     instrument  Instrument Sans     editorial display sans
     schibsted   Schibsted Grotesk   neo-grotesque, worked hard
     plex        IBM Plex Sans       technical institution

   Figtree is the default after testing rather than on paper. Schibsted Grotesk
   was the pick until it was rendered: its Google Fonts build carries very wide
   sidebearings on the comma and the full stop, so at display size every
   sentence shows a visible gap before its own punctuation. Verified against
   the other three, which are all correct. That is a disqualifying flaw in a
   face whose whole job here is to set large sentences, and it would need a
   different source or a manual kern fix to use.
   ────────────────────────────────────────────────────────────────────────── */

:root {
  --sans: 'Figtree', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    'Helvetica Neue', Arial, sans-serif;
}

html[data-font='schibsted'] {
  --sans: 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI',
    Roboto, 'Helvetica Neue', Arial, sans-serif;
}

html[data-font='instrument'] {
  --sans: 'Instrument Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI',
    Roboto, 'Helvetica Neue', Arial, sans-serif;
}

html[data-font='figtree'] {
  --sans: 'Figtree', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    'Helvetica Neue', Arial, sans-serif;
}

html[data-font='plex'] {
  --sans: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    'Helvetica Neue', Arial, sans-serif;
}

/* ── Tokens ────────────────────────────────────────────────────────────────
   Cool clinical neutrals. Warm greys are banned: they read hospitality, and
   this is a company that publishes numbers about medicines.
   One accent, and it is the blue already burning in the render. */

:root {
  color-scheme: light;

  --page: #ffffff;
  --tint: #f4f7fa; /* alternating band, and the only second surface */
  --ink: #0d1720; /* near-black, blue cast, never #000 */
  --muted: #4a5763; /* 7.4:1 on white */
  --muted-soft: #66727e; /* 4.8:1 on white, large text and leads only */
  --line: #e4e9ef;
  --line-strong: #cfd8e2;

  /* THE PRIMARY IS THE LOGO'S OWN BLUE. Client, 2026-08-18: follow the colour
     of the Panoptic logo as the primary, and use it everywhere.

     #1c3a8e is sampled from assets/panoptic-logo.webp itself, not chosen to
     look like it: the mark's chromatic pixels cluster tightly between #18368a
     and #1e3c90 and this sits in the middle of that cluster. It measures
     10.6:1 against white in both directions, which is the reason it can do a
     job the old #1b5fd0 could not. That one was 5.8:1, which is AA for body
     text and nothing more, so it could never be trusted under small white
     type or beside a hairline. This one clears AAA both ways.

     It is also a better colour for the job. A saturated mid blue reads as a
     highlight, which is why a page full of them looks like a template. A dark
     desaturated blue reads as ink that happens to have a hue, and sits on the
     page like type. */
  --brand: #1c3a8e; /* the logo, sampled. 10.3:1 on white, measured */
  --link: var(--brand);
  --link-strong: #142a6b; /* the same hue pressed, for hover and active */

  /* NOT the primary, and it must not be repointed at it. #3d8efc is the
     PRODUCT's own accent, C.teal in features/biotower/tokens.ts, and it is
     what the LED strips in the render are painted with. It appears in this
     deck only where a product surface is being reproduced faithfully. At
     3.2:1 it is for rules, markers and large type, never for body text. */
  --accent: #3d8efc;

  --vacuum: #070a0f; /* the render's sky, so its edge disappears */

  /* One radius rule, documented: surfaces 14, controls 10. Nothing pill. */
  --r-surface: 14px;
  --r-control: 10px;

  /* Soft, layered, low-alpha, tinted to the ink hue. Never for emphasis. */
  --shadow: 0 1px 2px rgb(13 23 32 / 0.04), 0 10px 30px -14px rgb(13 23 32 / 0.1);

  --shell: 1180px;
  --measure: 66ch;
  --section: clamp(4.5rem, 9vw, 7.5rem);
}

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

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

body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 18px;
  line-height: 1.65;
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a {
  color: var(--link);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

a:hover {
  text-decoration-thickness: 2px;
}

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

img {
  max-width: 100%;
}

/* ── Type scale ────────────────────────────────────────────────────────────
   Five sizes. Display sits at weight 500, not 700: at this scale the extra
   weight reads as shouting, and hierarchy is already carried by size. */

h1,
h2,
h3 {
  margin: 0;
  font-weight: 500;
  letter-spacing: -0.03em;
}

/* text-wrap: balance is what keeps a display line from stranding one word on
   its own row. It also means the scale can be set for size rather than for the
   worst-case break, which is why these caps are lower than they look: at 4rem
   on a 1440 viewport the hero headline still fills two full lines. */
.d-xl {
  font-size: clamp(2.4rem, 4.8vw, 4rem);
  line-height: 1.02;
  letter-spacing: -0.035em;
  text-wrap: balance;
}

.d-lg {
  font-size: clamp(2.2rem, 4.2vw, 3.4rem);
  line-height: 1.05;
  text-wrap: balance;
}

h2,
.d-md {
  font-size: clamp(1.8rem, 3vw, 2.5rem);
  line-height: 1.12;
  letter-spacing: -0.028em;
  text-wrap: balance;
}

h3 {
  font-size: 1.2rem;
  line-height: 1.35;
  font-weight: 600;
  letter-spacing: -0.012em;
}

p {
  margin: 0;
  max-width: var(--measure);
  text-wrap: pretty; /* no single-word last line, no two-character orphan */
}

.lead {
  font-size: clamp(1.15rem, 1.5vw, 1.3rem);
  line-height: 1.55;
  color: var(--muted-soft);
  max-width: 46ch;
  text-wrap: balance;
}

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

.small {
  font-size: 0.95rem;
}

/* The only uppercase on the page, and it is rationed to two instances.
   Set in the page sans, never a monospace: this deck has no terminal in it. */
.label {
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ── Layout helpers ────────────────────────────────────────────────────────*/

.shell {
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0 28px;
}

.shell-narrow {
  max-width: 900px;
  margin: 0 auto;
  padding: 0 28px;
}

section {
  padding: var(--section) 0;
}

.band-tint {
  background: var(--tint);
}

.rule {
  height: 1px;
  background: var(--line);
  border: 0;
  margin: 0;
}

/* Removed from view, kept in the accessibility tree. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── THE ARROW IS AN SVG, AND IT IS THE ONE THAT USED TO SHIP ──────────────
   NEITHER ARROW THIS DECK WANTS IS IN FIGTREE. Verified 2026-08-25 against
   the css2 endpoint the pages link: the latin subset runs U+0000-00FF and
   U+2000-206F and then cherry-picks U+2191 and U+2193 by hand -- and stops.
   U+2192 and U+2197 are in no subset it serves. So every arrow on this site
   was drawn by whatever the operating system substituted.

   WHAT IT SUBSTITUTED, MEASURED RATHER THAN GUESSED. On macOS the cascade
   lands on `system-ui`, which is Apple's San Francisco: U+2197 there has an
   advance of 1565/2048 = 0.7642em, and rendering the page's own font stack
   and `system-ui` to a canvas at 120px produced BYTE IDENTICAL PNGs. That is
   the arrow everyone has been looking at for months and the one the client
   asked to keep. It is also the reason this was invisible for months: macOS
   substitutes that, iOS and Android substitute hairlines from somewhere else.

   SO IT IS REDRAWN, NOT BORROWED. San Francisco is licensed for Apple
   platforms and its outlines are not ours to serve, and we do not need them:
   the glyph is two round capped strokes and its geometry is just numbers.
   Read out of the font with fontTools and rebuilt at 24 units to the ink box:

     shaft   a 45 degree stroke, font units (282,220) to (1283.5,1221)
     head    a 90 degree corner, arms to (495,1221) and (1283.5,431)
     stroke  162 units, round cap and round join

   U+2192, for the badge on a link that does not leave the site, is the SAME
   construction in the same font -- a shaft plus a 90 degree chevron, the same
   162 unit stroke -- so the two read as one family. It is built on the same
   unit scale further down this file, which is what keeps the two stroke
   weights equal rather than merely similar. Both are regenerable: the
   derivation is in the commit for 2026-08-25.

   A ROTATED U+2191 WAS TRIED FIRST AND REJECTED, on 2026-08-25, by the person
   who has to look at it. Figtree's arrow is a different design -- lighter,
   with a triangular head -- so turning it 45 degrees gives a correct arrow
   that is not this one. The lesson is that the glyph being IN the subset was
   never the requirement; matching what shipped was.

   THE BOX IS THE SIZE THE OLD GLYPH OCCUPIED, deliberately. 0.7642em is San
   Francisco's advance for U+2197 and 0.5688em by 0.5681em is its ink, so the
   arrow lands where it landed before and nothing measured against it moved --
   including the widths at the foot of this file. The span is `aria-hidden` in
   the markup and the SVG inherits that, so nothing here is announced; the
   `.vh` span beside it still carries "(external site, opens in a new tab)".
   The variation selector is gone with the glyph and is not needed: an SVG
   cannot be promoted to emoji. */
/* The gap is set in em so it tracks the label rather than being one value that
   is right in the nav and mean on a 48px button. 0.34em is about 6px at body
   size, against 3.6px before, which is enough to read as a pair rather than as
   a glyph stuck onto the last letter.

   The arrow itself was undersized: at 0.82em it sat visibly smaller than the
   text it belongs to, and this arrow is one of the three cues that a link
   leaves the site, so it should not be the quietest thing in the label. */
.ext {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 0.7642em;
  height: 0.5688em;
  /* San Francisco sets this arrow entirely ABOVE the baseline: its ink starts
     138.5/2048 = 0.0676em up. An inline-flex box takes its baseline from its
     one child, and a replaced child's baseline is its bottom edge, so without
     this the arrow would sit ON the baseline and drop by that much. */
  vertical-align: 0.0676em;
  margin-left: 0.34em;
  font-size: 0.92em;
  line-height: 1;
}

/* Sized here rather than at 100% because `.btn .ext` below turns this box into
   a 32px circle, and a badge is not a bigger arrow -- it is the same arrow in
   a disc. In em, so both cases are one rule: 0.5688em of whatever font-size
   the span was given is San Francisco's own ink box, in the nav at 0.92em and
   in the badge at 0.85em. */
.ext svg {
  display: block;
  width: 0.5688em;
  height: 0.5688em;
}

/* ── Buttons ───────────────────────────────────────────────────────────────
   Filled with --link, never --accent: white on --accent measures 3.2:1 and
   fails AA for a label at this size. White on --link is 10.3:1 since --link
   became --brand on 2026-08-18; it was 5.8:1 under the old #1b5fd0. */

/* THE PAIR. Client picked option 6 from buttons.html on 2026-08-18: a primary
   and a secondary built the same way, so they read as a set rather than as two
   things borrowed from different sites.

   FOUR THINGS make this button, and the old one did none of them:

   1. TWO SHAPES, NOT ONE. A capsule with a circle inside it. The circle is a
      second surface at the opposite value, so the button has an internal
      hierarchy: first the label, then the action. A flat rectangle with a
      glyph tacked on the end is one shape and reads as a div someone coloured.
   2. THE FILL IS DARK AND DESATURATED, which is what --brand now is.
   3. ASYMMETRIC PADDING. Air on the label side, a margin on the badge side,
      because the badge is its own object and not the end of a gutter. Equal
      padding is the tell that a button was laid out by a rule and not by eye.
   4. SIZE. Confidence is mostly size.

   THE BADGE NEEDS NO NEW MARKUP, which is why 51 buttons across 22 pages did
   not have to be touched. Every outbound button in this deck ALREADY carries
   `<span class="ext">` holding the arrow, in exactly the position the badge
   goes, so that span becomes the badge. Buttons that stay on the site have no
   `.ext`, so `:not(:has(.ext))::after` grows one for them with a straight
   arrow instead of an outbound one. The distinction is not decorative: an
   arrow leaving the frame means the link leaves the site, and that has been
   one of this deck's three outbound cues from the start.

   EVERYTHING IS IN em, so the three sizes this deck uses all work from one
   rule: 42px in the sticky header, 48px inline, 62px for .btn-xl in the
   closing band. Nothing about the badge is measured in px.

   RADIUS: PILL, and this is the one thing worth re-reading. The deck was told
   to drop pills on 2026-08-14 and told again on 2026-08-18 for the tower, and
   there are no pills anywhere else: chips are 8, controls 10, surfaces 14.
   This is a pill because the reference the client called amazing is one and
   because they picked it after seeing every option at both radii. A pill on a
   BUTTON and a pill on a STATUS CHIP are different objects. To undo it, change
   the one `border-radius` below to `var(--r-control)`. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Tightened 2026-08-18: the client called the first pass too padded. The
     badge is its own object with its own visual weight, so it does not need
     a gutter around it as well; 5px is the margin that keeps it off the
     capsule's edge and nothing more. */
  /* HEIGHT AND HORIZONTAL PADDING ARE SEPARATE DIALS, and the first pass at
     "less padding" moved both. The button was too tall; the label was never
     too close to the left edge, and at 0.9em it was. So the height stays down
     at 44 and the label side goes back up.

     The asymmetry is the thing to protect: 1.3em of air before the label,
     4px after the badge. The badge is its own object with its own edge, so it
     needs a margin, not a matching gutter. Equal padding on both sides is the
     tell that a button was laid out by a rule rather than by eye. */
  /* THE BADGE IS EVENLY INSET BY CONSTRUCTION, not by a number that has to be
     kept in step. One padding value on the top, the bottom and the right, and
     the badge STRETCHES to whatever height that leaves and stays square. So
     the ring of space around the circle is 6px on all three sides at every
     button size in the deck: 44px inline, 50px in the hero, 62px in a closing
     band. Nothing to re-tune when a size changes.

     The left is the exception and stays larger, because text needs more air
     beside it than a circle does. That is the only asymmetry left, and it is
     between the LABEL and the edge, not around the badge. */
  /* ONE VARIABLE DRIVES THE WHOLE SHAPE. --h is the button's height, --pad is
     the ring of space around the badge, and the badge is exactly --h minus
     twice --pad. So the gap above, below and to the right of the circle is
     --pad at every size in the deck, by arithmetic rather than by three
     numbers someone has to keep in step.

     The first attempt used `aspect-ratio: 1` with `align-self: stretch`, which
     LOOKS like the elegant version and is wrong: flex resolves a child's main
     size before stretch resolves its cross size, so the width came from a
     stale height and the badge overflowed the capsule's right edge by 6px.
     Measured, not guessed. Explicit arithmetic is the fix.

     The left is the one deliberate asymmetry, and it is between the LABEL and
     the edge, not around the badge: text needs more air beside it than a
     circle does. */
  /* THE ONE BUTTON. Client, 2026-08-18: a single agreed sizing and convention,
     locked to the button in the ask card, used everywhere.

     There is exactly one size in this deck now and NOTHING overrides it. The
     hero used to be 50px and the closing band 62px; both are gone, and the two
     classes that set them are empty shells kept only so the markup in twenty
     two files did not have to be edited.

     If a button ever genuinely needs to be bigger, change --h HERE and every
     button in the deck moves together. That is the point of one variable. Do
     not add a per page override: that is what produced three different buttons
     on one page and got this rule written. */
  --h: 44px;
  --pad: 6px;

  gap: 0.55em;
  min-height: var(--h);
  /* LEFT INSET REDUCED 1.35em to 1.05em on 2026-08-18. Client: "for each of
     the buttons can you slightly reduce that left spacing to make it more
     even with the padding on the right. only reduce the spacing on the left".

     ONLY the left changed. --pad still sets the top, the bottom and the
     right, so the badge stays evenly inset by construction and the height is
     untouched: this is a 4.8px move at a 16px root and nothing else shifts.

     The asymmetry is deliberate and survives, just smaller. It exists because
     the two sides are not the same problem: on the left a piece of TEXT meets
     the capsule edge and needs air, on the right a BADGE meets it, and a
     badge is its own object with its own edge, so it takes a margin rather
     than a matching gutter. Setting both to one number is the tell that a
     button was laid out by a rule rather than by eye. 1.05em is the smallest
     value that still reads as air rather than as a crop; an earlier pass went
     to 0.9em and the client rejected it as too tight. */
  padding: var(--pad) var(--pad) var(--pad) 1.05em;
  border: 1px solid transparent;
  border-radius: 999px;
  font-family: inherit;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.01em;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color 160ms ease,
    border-color 160ms ease,
    color 160ms ease,
    transform 90ms ease;
}

/* The badge. Two selectors, one appearance: the span that is already there,
   and the pseudo element for buttons that have no span. */
.btn .ext,
.btn:not(:has(.ext))::after {
  display: grid;
  place-items: center;
  width: calc(var(--h) - 2 * var(--pad));
  height: calc(var(--h) - 2 * var(--pad));
  flex: none;
  margin-left: 0;
  border-radius: 50%;
  font-size: 0.85em;
  line-height: 1;
  /* The badge moves, not the button. A small nudge on the mark inside a circle
     that stays put is the difference between acknowledging the pointer and
     jumping at it. */
  transition:
    background-color 160ms ease,
    color 160ms ease,
    transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* ── The badge on a link that STAYS on the site ────────────────────────────
   A straight arrow, and the same arrow: San Francisco's U+2192 is the same
   construction as its U+2197 above -- a shaft, a 90 degree chevron, the same
   162 unit stroke -- redrawn on the SAME unit scale, which is what makes the
   two stroke weights equal rather than merely close. 29.97 by 24.72 units is
   its ink box at that scale, and 0.7104em by 0.5859em is what those units are
   worth, so this badge and the outbound one are one family at every size.

   WHY THIS ONE IS A BACKGROUND AND THE OTHER IS MARKUP, and it is not a
   preference. `::after` is the badge for the buttons that have no span, which
   is the whole point of it -- 51 buttons across 22 pages never had to be
   touched. A pseudo element cannot contain an `<svg>` and cannot have a pseudo
   element of its own, so the arrow has to be painted by the box that is
   already painting the disc. A `mask` would take the disc with it, because a
   mask clips everything the element paints. That leaves a background image,
   and a background image cannot read `currentColor`.

   So the colour is baked, and it is baked THREE TIMES because that is how many
   colours this badge has: the arrow is --brand on the primary's white disc,
   --link-strong when that button is hovered, and #fff on the secondary's dark
   disc. Change a colour below and change it here too; there is no way to make
   these follow automatically, which is exactly why the outbound arrow, which
   CAN follow, is markup. */
.btn:not(:has(.ext))::after {
  content: '';
  background-repeat: no-repeat;
  background-position: center;
  background-size: 0.7104em 0.5859em;
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29.97 24.72' fill='none' stroke='%231c3a8e' stroke-width='3.34' stroke-linecap='round' stroke-linejoin='round'><path d='M1.67 12.36h26.63M17.61 1.67 28.3 12.36 17.61 23.05'/></svg>");
}

/* The nudge. No rotation to compose with any more, so it is the plain 2px it
   was always meant to be. */
.btn:hover .ext,
.btn:not(:has(.ext)):hover::after {
  transform: translateX(2px);
}

.btn:active {
  transform: translateY(1px);
}

.btn-primary {
  background: var(--brand);
  color: #fff;
}

/* `background-color`, NOT the `background` shorthand: the shorthand resets
   background-image, and on the pseudo element that image IS the arrow. */
.btn-primary .ext,
.btn-primary:not(:has(.ext))::after {
  background-color: #fff;
  color: var(--brand);
}

.btn-primary:hover {
  background: var(--link-strong);
}

.btn-primary:hover .ext,
.btn-primary:not(:has(.ext)):hover::after {
  color: var(--link-strong);
}

.btn-primary:not(:has(.ext)):hover::after {
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29.97 24.72' fill='none' stroke='%23142a6b' stroke-width='3.34' stroke-linecap='round' stroke-linejoin='round'><path d='M1.67 12.36h26.63M17.61 1.67 28.3 12.36 17.61 23.05'/></svg>");
}

/* The secondary inverts the badge rather than dropping it: a dark circle on a
   light button. That is what keeps the two reading as one set. */
.btn-secondary {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--ink);
}

.btn-secondary .ext,
.btn-secondary:not(:has(.ext))::after {
  background-color: var(--ink);
  color: #fff;
}

/* White on the dark disc. The one place the baked colour differs. */
.btn-secondary:not(:has(.ext))::after {
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29.97 24.72' fill='none' stroke='%23ffffff' stroke-width='3.34' stroke-linecap='round' stroke-linejoin='round'><path d='M1.67 12.36h26.63M17.61 1.67 28.3 12.36 17.61 23.05'/></svg>");
}

.btn-secondary:hover {
  border-color: var(--ink);
}

/* No `:has()` is a 2023 browser. The fallback is the button without a badge,
   which is a complete control and not a broken one, so nothing is gated on it.
   The padding comes back to symmetric there so it does not sit lopsided. */
@supports not selector(:has(*)) {
  .btn {
    padding: 10px 18px;
    gap: 0.34em;
  }

  .btn .ext {
    width: auto;
    height: auto;
    background: none;
    color: inherit;
    border-radius: 0;
    font-size: 0.92em;
  }
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

/* ── The render plate ──────────────────────────────────────────────────────
   The one photographic element on the page. Its own stacking context so any
   overlay it carries stays local and never joins a z-index argument. */

.plate {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-radius: var(--r-surface);
  background: var(--vacuum);
}

.plate img {
  display: block;
  width: 100%;
  height: auto;
}

.plate-full {
  border-radius: 0;
}

figure {
  margin: 0;
}

figcaption {
  padding-top: 14px;
  font-size: 0.92rem;
  color: var(--muted);
}

/* ── Header ────────────────────────────────────────────────────────────────*/

.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: rgb(255 255 255 / 0.86);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

.site-header .shell {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 32px;
  min-height: 74px;
  padding-top: 13px;
  padding-bottom: 13px;
}

/* The real lockup, cropped out of frontend/public/imports/panoptic-logo-light.png
   and re-encoded at 2x for this deck. It carries the words as well as the mark,
   so the text node beside it is now there only to give the link its accessible
   name: font-size 0 takes it out of the picture without taking it out of the
   accessibility tree, which is what `display: none` or a colour trick would do.

   Sized by height with the width left to the aspect ratio, so a re-export at a
   different crop cannot stretch it. */
.wordmark {
  display: inline-flex;
  align-items: center;
  font-size: 0;
  color: var(--ink);
  text-decoration: none;
}

/* SCOPED TO THE SITE HEADER, and the scope is the point rather than tidiness.
   `.wordmark` is a SHARED component: the site header uses it, and so does the
   product console's own brand band, `.rail-brand .wordmark`, which is
   reproduced inside the two preview windows on this page. An unscoped
   `padding: 2px 0` here took that band from a measured 55px to 59px, and the
   rail is a fixed `auto minmax(0, 1fr) auto` grid, so the 4px came out of the
   nav band and every row under it moved. assets/console.css had to grow a
   counter-rule to undo it. This scope is what makes that counter-rule
   unnecessary rather than what makes it work.

   2px of padding, and it is a TOUCH TARGET rather than a spacing decision. The
   lockup is 40px tall, so as a bare link the header's first control measured
   40px against the 44px minimum. The padding is invisible: the bar's height
   comes from `min-height: 74px` on the shell and 44 is still well inside the
   48px of room the comment below documents. */
.site-header .wordmark {
  padding: 2px 0;
}

/* 40px, up from 30, and the header does NOT grow with it. The bar's height is
   set by `min-height: 74px` on `.site-header .shell` plus 13px of padding top
   and bottom, so the lockup has 48px of room before it starts pushing: at 40
   the content box is 66px and the min-height is still what is winning. Do not
   go past 48 without changing the min-height too, and do not add padding here
   to compensate, because that is what would make the nav taller.

   Sized by height with the width left to the aspect ratio, so a re-export at
   a different crop cannot stretch it. */
/* THE LOCKUP IS 540x160 SINCE 2026-08-19 AND IT WAS 216x64. Client, with
   the header on screen: "The top left logo should be higher definition/
   higher quality." He was right and the number says why: the header draws it
   at 135x40 CSS px, so the old file was 1.6x, BELOW the 2x a retina display
   asks for, and every Mac and every phone was upscaling it.

   540x160 is 4x the header's box and 4.4x the footer's, so both are sharp on
   a 3x screen with headroom. It costs 7.9 kB.

   REDRAWN FROM THE MASTER, NOT UPSCALED. The source is the product repo's
   panoptic-logo-light.png, 1702x436, cropped to its own ink and resampled
   once. Two things changed with it, both deliberate: the artwork is no longer
   stretched (the old file was 4.6% wide of the master's proportions, which is
   why its ring read thin), and the ink now sits on a transparent canvas at
   the same 3.375 ratio and the same vertical inset as before, so this rule
   and the footer's did not have to move. `background-position: left center`
   means the slack on the right is never seen. */
.wordmark .mark {
  width: calc(40px * 3.375);
  height: 40px;
  flex: none;
  background: url('panoptic-logo.webp') left center / contain no-repeat;
}

/* The old mark drew itself out of a square and a dot. The lockup replaces both. */
.wordmark .mark::after {
  content: none;
}

.header-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 30px;
}

nav.site-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 26px;
  font-size: 0.98rem;
}

nav.site-nav a {
  color: var(--muted);
  text-decoration: none;
  padding-bottom: 3px;
  border-bottom: 2px solid transparent;
  transition: color 160ms ease;
}

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

nav.site-nav a[aria-current='page'] {
  color: var(--ink);
  font-weight: 500;
  border-bottom-color: var(--accent);
}

/* THE HEADER BUTTON IS THE SAME BUTTON. It used to be 42px with its own
   padding and its own type size, which was a third button on a page that is
   only allowed one. The header has room for 44px: the bar's own min-height is
   74px and the lockup beside it is 40. */
.site-header .btn {
  /* intentionally empty, see the comment on .btn above */
}

/* ── The primary's label, long and short ───────────────────────────────────
   THE SHORT FORM IS OFF EVERYWHERE BY DEFAULT and is switched on in exactly
   one place, the `max-width: 359px` block at the foot of this file. The
   button carries both labels in the markup, so this rule is what stops the
   two rendering as "Book a demoDemo" in the 23 deck files that do not reach
   that width, and in every browser at every other width.

   `display: none` and not a visual hiding, and the asymmetry with `.cta-full`
   below is deliberate. The SHORT form is decoration: it duplicates a label
   that is already in the tree, so a screen reader must never meet it, and it
   carries `aria-hidden` in the markup as well. The LONG form is the real
   accessible name and is only ever hidden from view. */
.cta-brief {
  display: none;
}

/* ── The menu control ──────────────────────────────────────────────────────
   NOT A BUTTON, and that is deliberate rather than a technicality. `.btn` is
   locked deck wide at one size and one shape by explicit client instruction,
   and every override of it is an empty shell. This is a disclosure control,
   it appears at one width band and never beside the primary as a second ask,
   so it gets its own class and takes nothing from `.btn` except the 44px
   height, which is the touch minimum rather than the button's size.

   IT IS A WORD, NOT A HAMBURGER. DESIGN.md section 4 says no icons anywhere
   and bans hand rolled decorative SVG, and it is right: three stacked bars
   would be the only glyph on a page that has none. "Menu" is unambiguous,
   localisable, and readable by anyone who has never learnt what the bars mean.
   The caret is a state indicator rather than an icon, it is two borders rather
   than a drawing, and it is the one cue that says the control opens something.

   It is a <summary> inside a <details>, so it needs no JavaScript: `page.html`
   deliberately loads none, and the deck's standing rule is that everything
   works with scripting off. That also buys the right semantics for free, a
   real keyboard control and an announced expanded state. */
/* Both are off outside the narrow band. The <details> holds only its summary,
   so hiding it on a wide screen removes nothing: the nav it controls is its
   SIBLING and is laid out by the desktop rule as it always was. */
.nav-menu {
  display: none;
}

.nav-toggle {
  display: none; /* only exists in the narrow band; switched on below */
  align-items: center;
  gap: 0.5em;
  min-height: 44px;
  padding: 6px 16px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-control);
  color: var(--ink);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.01em;
  white-space: nowrap;
  cursor: pointer;
  list-style: none; /* the default disclosure triangle */
  transition: border-color 160ms ease;
}

.nav-toggle::-webkit-details-marker {
  display: none;
}

.nav-toggle:hover {
  border-color: var(--ink);
}

/* Two borders meeting, rotated. Not a drawing and not an asset. */
.nav-toggle::after {
  content: '';
  width: 0.42em;
  height: 0.42em;
  margin-top: -0.22em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  transition: transform 180ms ease;
}

.nav-menu[open] > .nav-toggle::after {
  margin-top: 0.16em;
  transform: rotate(-135deg);
}

/* ── Shared blocks: doors, provenance, facts, closing band, footer ─────────
   These four sections are identical across the directions on purpose. The
   comparison is about the hero and the stack; holding the tail constant is
   what makes it a fair one. */

.doors {
  display: grid;
  gap: 0;
  border-top: 1px solid var(--line);
}

.door {
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr) auto;
  gap: 8px 36px;
  align-items: center;
  padding: 30px 8px;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  color: inherit;
  transition: background-color 160ms ease, padding-left 160ms ease;
}

.door:hover {
  background: var(--tint);
  padding-left: 18px;
}

.door p {
  color: var(--muted);
  font-size: 1rem;
}

.door .host {
  color: var(--link);
  font-size: 0.98rem;
  white-space: nowrap;
}

.door:hover .host {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.method {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.55fr);
  gap: 32px 56px;
  align-items: start;
}

.method-sources {
  display: grid;
  gap: 10px;
  margin: 0;
  padding: 0 0 0 18px;
  list-style: none;
  border-left: 2px solid var(--accent);
  font-size: 1rem;
}

dl.facts {
  display: grid;
  grid-template-columns: minmax(11rem, max-content) minmax(0, 1fr);
  gap: 0;
  margin: 0;
}

dl.facts dt {
  padding: 18px 0;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.95rem;
}

dl.facts dd {
  margin: 0;
  padding: 18px 0;
  border-top: 1px solid var(--line);
}

address {
  font-style: normal;
}

.closing {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 22px 40px;
  padding: clamp(32px, 5vw, 52px);
  background: var(--tint);
  border-radius: var(--r-surface);
}

.closing h2 {
  max-width: 18ch;
}

/* ── Footer ────────────────────────────────────────────────────────────────
   An ordinary site footer, which is what it should have been from the start.
   The two line strip it replaces carried one policy link and nothing else, so
   the legal and policy pages had nowhere to live and the deck kept trying to
   invent a block on the page for them. A footer is where those links go on
   every site, and the reader already knows to look there.

   Four columns: the company, then the three groups a reader actually reaches
   for. Products carries the outbound hosts, Company carries the pages on this
   site, Legal carries the policies. Below 900px they stack into two, below
   620px into one, because four columns of three links is a phone problem. */
.site-footer {
  border-top: 1px solid var(--line);
  padding: 54px 0 40px;
  color: var(--muted);
  font-size: 0.95rem;
}

.site-footer .shell {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr));
  gap: 40px 32px;
}

/* 36px, up from 26, following the header's move to 40. The footer lockup is
   allowed to be a little smaller than the header's because it is the last
   thing on the page rather than the first, but 26 against 40 read as two
   different logos. Nothing else in the footer is sized off this, so it grows
   the brand column and no other. */
.foot-brand .mark {
  display: block;
  width: calc(36px * 3.375);
  height: 36px;
  background: url('panoptic-logo.webp') left center / contain no-repeat;
}

.foot-brand p {
  margin-top: 14px;
  max-width: 30ch;
}

/* A column heading, not an eyebrow. DESIGN.md rations uppercase wide tracked
   labels to two on the whole page and spends both on the SOURCES lists, so
   three more of them in the footer would break a budget the page has already
   committed. Sentence case at normal tracking reads quieter and does the same
   job, which is to name the column. */
.foot-col h2 {
  font-size: 0.9rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--muted);
  margin-bottom: 14px;
}

.foot-col ul {
  display: grid;
  gap: 9px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* inline-block plus vertical padding, so the hit area clears the 24px WCAG
   2.2 target-size minimum. As bare inline links these measured 18 to 19px
   tall at every viewport. The list already had a 9px gap between rows, so the
   padding lands inside space the layout was leaving empty and the footer does
   not grow. */
.foot-col a {
  display: inline-block;
  padding: 3px 0;
  color: var(--ink);
  text-decoration: none;
}

.foot-col a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* A policy that does not exist yet is named and marked, not linked. A link to
   a page that is not written is worse than an honest gap: it tells a reader
   the document exists.

   THE MARK IS THE WHOLE MECHANISM, SO IT HAS TO BE LEGIBLE. This is the only
   thing standing between the Legal column and a reader concluding that a
   Privacy Policy has been written. MEASURED in Chromium 149: the note was set
   in --line-strong, #cfd8e2, which is the HAIRLINE token, and against the
   footer's white it computes 1.44:1 at 12.48px. WCAG 1.4.3 wants 4.5:1 for
   text this size, and 1.44 is not a faint mark, it is invisible ink. What a
   sighted reader actually saw was three names in a list, one of them slightly
   grey, and nothing telling them the middle one does not exist. The whole
   comment above was true of the intention and false of the rendering.

   --muted, #4a5763, measures 7.41:1 here. Nothing else changes: same content,
   same 0.78rem, same block display, same position, so the footer's geometry
   is untouched and this is legibility rather than a restyle of a locked
   component. LEGAL-PAGES.md section 6 rule 2 is the rule this serves.

   The words still come from CSS, which means they are absent from
   textContent, from a copy and paste, from a Ctrl+F, and from the page with
   the stylesheet blocked. Chromium does put them in the accessibility tree as
   StaticText, so a screen reader hears them, but the robust fix is markup and
   the markup is the locked footer's, repeated by hand in 30 files. That one
   is written up for the client rather than taken here. */
.foot-col .soon {
  color: var(--muted);
}

.foot-col .soon::after {
  content: 'Coming soon';
  display: block;
  font-size: 0.78rem;
  color: var(--muted);
}

.site-footer .foot-base {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 24px;
  justify-content: space-between;
  padding-top: 26px;
  border-top: 1px solid var(--line);
}

@media (max-width: 900px) {
  .site-footer .shell {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .foot-brand {
    grid-column: 1 / -1;
  }

  /* MEASURED: `padding: 3px 0` on a 25.1px line box makes a 31.1px target, not
     a 44px one. It clears WCAG 2.2's 24px minimum, which is what the comment
     above it claims and all it ever claimed, and it misses the 44px figure the
     phone and tablet pass is held to.

     10px each side makes it 45.1px, and the row gap comes off to pay for it,
     because two 45px targets touching need no gap between them to be
     distinguishable. Pitch goes from 40.1px to 45.1px, so a four link column
     grows 20px on a phone and the whole footer grows about 80px, on the one
     layout where there is height to spare and none of it is above the fold.

     SCOPED TO THE COLLAPSED FOOTER ON PURPOSE. The client called this footer
     perfect and locked it, so the four column desktop arrangement they were
     looking at is untouched: at 901px and up these links are exactly what they
     were. This changes only the layout that is already a different one. */
  .foot-col ul {
    gap: 0;
  }

  .foot-col a {
    padding: 10px 0;
  }
}

@media (max-width: 620px) {
  .site-footer .shell {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ── Reveal on enter ───────────────────────────────────────────────────────
   Only applied when scripting is on, so a JS-less client sees finished
   content rather than an empty page waiting for a class that never lands. */

html.js .reveal {
  opacity: 0;
  transform: translateY(18px);
  transition:
    opacity 620ms cubic-bezier(0.16, 1, 0.3, 1),
    transform 620ms cubic-bezier(0.16, 1, 0.3, 1);
}

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

/* ── Responsive ────────────────────────────────────────────────────────────*/

@media (max-width: 900px) {
  .method,
  .door {
    grid-template-columns: minmax(0, 1fr);
  }

  .door {
    gap: 6px;
    padding: 24px 8px;
  }

  dl.facts {
    grid-template-columns: minmax(0, 1fr);
  }

  dl.facts dt {
    padding-bottom: 0;
    border-top: 1px solid var(--line);
  }

  dl.facts dd {
    padding-top: 4px;
    border-top: 0;
  }
}

/* ── The header's two row band ─────────────────────────────────────────────
   MEASURED, not guessed. The bar holds a 147.5px lockup, a 474.6px six item
   nav and a 162.8px button, with a 32px gap between each pair and 56px of
   shell padding: 903px, and Chromium puts it on one row at exactly 903 and
   wraps at 902. So the one row layout is a >=903px layout and every width
   below that needs the other one.

   What it was wrapping into was an accident, and TODO.md section 8 recorded
   only half of it as "two rows between about 640 and 900". Measured across the
   whole band, before this rule:

     360      4 rows, 205.6px   lockup, button, and the nav over two rows
     390-414  3 rows, 153.6px
     621-767  3 rows, 166.9px   nav on its own row AND the button on its own
     768-902  2 rows, 124.0px   the lockup alone on row one, 560px of nothing
                                beside it, nav and button underneath

   167px of a 1024px tall tablet, on a bar that is `position: sticky`, is a
   lid rather than a header.

   THE FIX IS NOT A NEW LAYOUT. The <=620px rule already had the right one and
   had it at the wrong breakpoint: promote both `.header-actions` children to
   flex items of the bar so `order` can reach them, put the button up beside
   the lockup where the empty 560px was, and give the nav the full width
   underneath. That arrangement is correct at every width where one row does
   not fit, so the breakpoint moves to where the measurement puts it. The DOM
   still reads nav-before-button, which is the tab order a wide screen wants.

   `.header-actions` and `nav.site-nav` are not scoped to `.shell`, so this
   also reaches a header built on `.shell-wide`, which layout-7-legora.html
   does. Only the shell's own gap below is scoped, and that page sets its own.
   ────────────────────────────────────────────────────────────────────────── */
@media (max-width: 902px) {
  .site-header .shell,
  .site-header .shell-wide {
    gap: 12px 16px;
    padding-top: 10px;
    padding-bottom: 10px;
  }

  .header-actions {
    display: contents;
  }

  .site-header .btn-primary {
    order: 1;
  }

  nav.site-nav {
    order: 2;
    width: 100%;
    gap: 2px 20px;
    font-size: 1rem;
  }

  /* 44px of touch target, and on this layout it is FREE: the nav has a full
     width row to itself here, so the padding lands in space the bar was
     leaving empty. 26.4px line box, 9 above, 7 below, 2 of border. The current
     page underline moves from 3px under the text to 7px, which is the whole
     visible consequence and it is an improvement on a row this size.

     As bare inline links these measured 29.8 to 30.9px tall at every width,
     which clears the 24px WCAG 2.2 minimum and misses the 44px one. */
  nav.site-nav a {
    padding: 9px 0 7px;
  }

  /* ── The disclosure, when the markup has one ─────────────────────────────
     GATED ON `:has()`, and that gate is the whole safety argument. The header
     markup lives in expand-2-dossier.html, which assemble-page.py copies into
     page.html, and this stylesheet is shared by twenty three files that do not
     all have it yet. A rule that hid `.site-nav` unconditionally would delete
     the navigation from every one of them the moment it shipped. So the nav is
     hidden only inside a header that actually contains a toggle, and a browser
     with no `:has()` support keeps the wrapped two row nav, which is the
     layout above and a complete header rather than a broken one.

     WHY THE NAV IS A SIBLING OF THE <details> AND NOT ITS CHILD. Inside it is
     the tidier markup and it does not survive the desktop: a closed <details>
     hides its content, and the only way to show it again at 903px and up is
     `::details-content { content-visibility: visible }`, which is a 2024
     selector. Measured in Chromium 149: with the nav inside, `display:
     contents` on the <details> does NOT reveal it, and only the pseudo does.
     If that pseudo is unsupported the desktop navigation disappears entirely,
     which is a catastrophic fallback rather than a degraded one. As a sibling
     revealed by `[open] ~`, every browser that has `:has()` has the rest, and
     every browser that does not keeps the wrapping nav. The cost is that the
     summary's announced expanded state is not structurally tied to the panel;
     `aria-controls` on the summary names it instead.

     WHY 903px. Measured, not chosen: the bar needs exactly 903px to hold the
     lockup, six nav items and the button on one row, and Chromium wraps at
     902. Below that the nav has to go somewhere, and a disclosure is where. */
  .site-header:has(.nav-menu) nav.site-nav {
    display: none;
  }

  .site-header .nav-menu {
    display: block;
    order: 2;
  }

  .site-header:has(.nav-menu) .btn-primary {
    order: 1;
    margin-left: auto; /* lockup left, button and menu together on the right */
  }

  .site-header .nav-toggle {
    display: inline-flex;
  }

  /* Open, it is a menu rather than a wrapped row of text: full width rows,
     one hairline each, 44px of target by construction. The bar grows to hold
     it, so nothing is trapped underneath the sticky header, and the same
     control closes it. */
  .site-header .nav-menu[open] ~ nav.site-nav {
    display: grid;
    order: 3;
    width: 100%;
    gap: 0;
    margin-top: 4px;
    border-top: 1px solid var(--line);
  }

  /* Every row indented by the same 12px, so the current page marker is the
     only thing that differs between them. Indenting only the marked row put
     one label 12px right of the other five, which reads as a mistake. */
  .site-header .nav-menu[open] ~ nav.site-nav a {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 4px 0 4px 12px;
    border-bottom: 1px solid var(--line);
  }

  /* The last row's rule and the header's own bottom border are the same line
     drawn twice, 4px apart. */
  .site-header .nav-menu[open] ~ nav.site-nav a:last-child {
    border-bottom: 0;
  }

  .site-header .nav-menu[open] ~ nav.site-nav a[aria-current='page'] {
    box-shadow: inset 2px 0 0 var(--accent);
  }
}

@media (max-width: 620px) {
  body {
    font-size: 17px;
  }

  .shell,
  .shell-narrow {
    padding: 0 20px;
  }

  .closing {
    padding: 26px;
  }
}
/* ── The mark alone, on a phone ────────────────────────────────────────────
   ARITHMETIC, not taste. A 360px screen has 320px of content width. The bar
   has to hold the lockup, the primary and the menu control on one row or it
   is two rows again, and the primary is 158px and locked. The full lockup is
   135px at 40px tall, so 135 + 158 + an 88.6px menu control and two 16px gaps
   is 413.6px against 320. It cannot be done.

   Dropping to the MARK gets there: 44 + 158 + 82.6 and two 10px gaps is
   304.6px, and every phone in the sweep clears it. It is the ordinary
   responsive treatment of a lockup, and the full one comes back at 466px.

   WHY 465 AND NOT 440. Measured 2026-08-25 in Chromium, on page-scroll.html,
   at every integer width from 380 to 560. The full lockup needs 135 + 158 +
   88.6 of content plus two 16px gaps and the shell's two 20px gutters, which
   is 453.6px. So from 441, where this query used to stop, to 453 the bar was
   holding the full lockup on TWO ROWS at 122px tall: the exact collapse this
   block exists to prevent, reintroduced thirteen pixels above its own
   ceiling. With fonts.gstatic blocked so the fallback face renders, the
   button and the toggle are about 4% wider and that band runs to 459. 465
   sits six pixels clear of the worse of the two, for the same reason 359 sits
   clear of 350 further down this file: a breakpoint that holds only while the
   webfont arrives is not a fix, it is the same bug on a timer.

   THE CROP IS ARITHMETIC TOO, and it needs no second asset. The numbers, and
   how to re-derive them when the artwork changes, are on the mark rule below.

   The accessible name is untouched: the "Panoptic Bio" text node beside the
   mark is still there at font-size 0, which is why it was written that way. */
@media (max-width: 465px) {
  /* `.site-header` and not `.wordmark`. Unscoped, this reached
     `.rail-brand .mark` inside both product preview windows, where
     assets/console.css sets a 96px box and no background-size of its own: a
     40px tall lockup is 147px wide, so the art was cut at two thirds and the
     company's name rendered as "Panop" on every phone. Measured on page.html
     before the scope went in. `.wordmark` is shared by the header, the footer
     and the console rail, so a bare rule on it reaches all three. */
  .site-header .wordmark .mark {
    /* THE WINDOW AND THE SHIFT, MEASURED AGAINST THE ASSET ON DISK.
       panoptic-logo.webp has been 540x160 since the 2026-08-19 re-export. Its
       ink runs x 44 to 515, the concentric mark is x 44 to 154, and the "P"
       of Panoptic begins at x 174. `background-size: auto 40px` scales by
       40 / 160 = 0.25, so in CSS pixels the mark occupies 11 to 38.75 and the
       letter starts at 43.5.

       A WINDOW ALONE CANNOT SHOW THIS MARK, and that is what broke it. The
       box crops from the LEFT edge, and everything before x 11 is the
       artwork's own transparent margin. This rule used to read `width: 34px`
       with no shift, a number measured against the 216-wide file that this
       one replaced, and a 34px window over an image whose mark ends at 38.75
       slices 4.75px off the ring. The closed rings rendered as a crescent on
       every viewport under the ceiling: 375, 390, 393, 412, 430, every phone
       in the sweep. It was live from 2026-08-19 to 2026-08-25.

       So the artwork is pulled left by its own margin first, and the window is
       then the mark's own width. -11px puts the ring flush at x 0, where it
       lines up with the content edge below it instead of sitting 11px inside
       it; 29px covers the ring's 27.75px with 1.25px to spare; and the "P",
       now at 32.5, is 3.5px outside the box. `background-size: auto 40px` is
       what makes the height drive the scale, and `contain` would fit the whole
       lockup into 29px and render it 8px tall.

       RE-DERIVE THESE, DO NOT NUDGE THEM BY EYE, IF THE ASSET CHANGES AGAIN.
       It has changed twice and broken this rule both times, silently, because
       a cropped mark still looks like a mark. The three numbers are the alpha
       bounding box of the mark and the left edge of the next glyph, scaled by
       40 / (image height). check-publish.py reads them out of the file and
       fails when they stop agreeing with what is written here. */
    width: 29px;
    background-position: -11px center;
    background-size: auto 40px;
  }

  /* The mark is now narrower than the 44px touch minimum, so the LINK carries
     the target instead of the artwork. Padding rather than a width, so the
     mark stays optically flush with the content edge below it. 29 + 15 is the
     same 44px of bar that 34 + 10 spent before it, so nothing downstream of
     the lockup moved when the crop was fixed. */
  .site-header .wordmark {
    min-height: 44px;
    padding-right: 15px;
  }

  .site-header .shell,
  .site-header .shell-wide {
    gap: 10px;
  }

  .nav-toggle {
    padding: 6px 13px;
  }
}

/* ── 320px: the primary keeps its size and gives up three words ────────────
   THE LAST WIDTH THE HEADER DID NOT COLLAPSE. At 320 the bar was 121px and
   two rows: the mark and the button on one, the menu control dropped onto a
   second. 320px is not a hypothetical, it is an iPhone SE 1st gen and any
   Android running large text at 320 CSS px.

   THE ARITHMETIC, and it is why this is a content change rather than a
   layout one. A 320px viewport gives 280px of content inside the shell's
   20px gutters. The bar has to hold a 44px mark, a 162.78px button and an
   82.59px menu control with two 10px gaps: 309.37px. It is 29.37px over
   before anything wraps, and it is over by 9.37px even with every gap and
   both gutters set to zero. Something on the row has to get smaller.

   FOUR CANDIDATES WERE MEASURED AT 320 AND THREE FAILED. Numbers are
   Chromium, page.html, with the webfont loaded and again with fonts.gstatic
   blocked so the fallback face renders, because a 320px phone is exactly
   where a webfont does not arrive and the fallback is about 4% wider.

     1. Squeeze the chrome. Gutters to 16, gaps to 8, the toggle's padding to
        10, and drop its caret: +3.34px of slack loaded, MINUS 2.21 on the
        fallback face. Gutters 12, gaps 6, toggle padding 8: +4.63 loaded,
        MINUS 0.92 on the fallback. Both of these "fit" only while the network
        cooperates, which is not a fix, it is the same bug on a timer. A 12px
        header gutter also stops the lockup lining up with the page content
        below it, at the one width where that misalignment is most visible.
     2. "Book demo" instead of "Book a demo". Saves 11.94px against a 29.37px
        deficit and does not fit on its own. With the full chrome squeeze on
        top it reaches +0.57px loaded and MINUS 4.37 on the fallback face.
     3. Move the menu control to the other side of the button. Worth EXACTLY
        ZERO px: the constraint is a sum and this is a reorder. It changes
        which item wraps, and the requirement is that nothing wraps.
     4. Let the mark stand alone. Already true here since the 440px block
        above, and its 44px is the touch minimum, so there is nothing left in
        it to give.

   WHAT IS LEFT is the label, and it is also the cheapest thing on the row.
   "Book a demo" is 92.39px of a 280px content width, a third of the whole bar
   spent on words beside a control that is already the only filled brand pill
   on the screen. "Demo" is 42.41px and lands the bar at 259.39px against 280:
   20.61px of slack loaded, 17.61px on the fallback face. That is the only
   candidate with room to survive a face it was not measured in.

   THIS IS NOT A SECOND BUTTON SIZE, and the distinction is the whole reason
   it is allowed. `--h` is still 44px and `--pad` still 6px here; the capsule,
   the badge, the 1.35em of air before the label and every other dial are
   untouched and inherited. What changed is the string inside it. The size is
   locked, the copy is not, and nothing in this file overrides `.btn`.

   359 AND NOT 360. Above this the long label fits with room: 360 needs
   309.37px of 320 loaded and 314.92 on the fallback face, and every width
   from 360 up is byte for byte what it was. The long label stops fitting at
   about 350px loaded and about 355 on the fallback, so the switch sits just
   clear of the band where it would be a hairline decision, and the whole
   320 to 359 range gets slack instead of a fit. There is no fractional
   viewport hazard: a 359.5px screen takes the LONG label and still has 4.6px
   to spare on the fallback face.

   SCOPED TO `.site-header`. `.cta-full` and `.cta-brief` are a generic
   mechanism but 359px is this bar's own arithmetic, and the same pair in the
   locked footer, which stacks into a full width column at 320 and has room
   to spare, would shorten for no reason. Same rule as `.wordmark` above:
   name the scope. */
@media (max-width: 359px) {
  /* VISUALLY hidden, not `display: none`, so the accessible name is still
     "Book a demo (opens in a new tab)" at 320. These are the properties of
     `.vh` near the top of this file, written out rather than shared, because
     `.vh` must stay unconditional and this is only true under one query.
     Absolutely positioned, so it also stops being a flex item and takes its
     0.55em `gap` with it: the button measures 112.8px here, which is the
     42.41px label plus the same padding, gap and badge as at 1440. */
  .site-header .cta-full {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  .site-header .cta-brief {
    display: inline;
  }
}

/* ── Reduced motion ────────────────────────────────────────────────────────
   The reduced experience is complete, not a broken version of the other. */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  html.js .reveal {
    opacity: 1;
    transform: none;
  }
}
