/* ============================================================================
   the Sapiom marketing site. Foundation, chrome, section rhythm.
   ----------------------------------------------------------------------------
   Product-first: the page is a quiet frame and the product artifacts are the
   loud part. Everything here stays deliberately plain so the mockup kit in
   mk.css can carry the page.

   Every colour, space, radius and curve resolves from ./ds/tokens.css. The one
   exception is the warm gradient below, which is this site's single accent and
   has exactly two jobs.
   ============================================================================ */

/* Geist and Geist Mono, both from ./ds/fonts.css, and nothing else. The site
   and the product it shows are one typographic system: a marketing headline in
   a face the app does not have makes every product crop read as a foreign
   object pasted onto the page. */

*, *::before, *::after { box-sizing: border-box }
button, input, select, textarea { font: inherit; color: inherit }
html, body { margin: 0 }
/* Every in-page anchor clears the sticky header. Without this an #id lands
   with its own heading hidden behind the nav. */
html { scroll-behavior: smooth; scroll-padding-top: calc(var(--nav-h) + var(--sp4)) }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto } }

body {
  --sans: var(--font);
  background: var(--marketing-bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* --------------------------------------------------------------------------
   THE SITE SCALE. Six steps, in rem, and the base is 1rem.

     --t-meta     0.75rem   12px   mono only: an eyebrow, a timestamp, a count,
                                   a tabular numeral in a dense row. This is
                                   the ONLY step below 14px and it is the only
                                   step a sans sentence may never use.
     --t-small    0.875rem  14px   CHROME, not prose: a nav item, a button
                                   label, a tag, a footer link, a table cell.
     --t-body     1rem      16px   THE BASE, and every sentence on the page.
                                   A paragraph, a card body, a list row's
                                   description, an FAQ answer. If it is a
                                   sentence a reader has to read, it is this.
     --t-title    18 -> 22px       a card title, a row title, an .h3, a block
                                   heading beside an artifact, and the hero
                                   lead. One step, because they are one job:
                                   naming the thing next to it.
     --t-h2       30 -> 42px       a section heading.
     --t-display  38 -> 60px       the hero and the closer, once per page.

   The site used to run body copy at 14px and secondary copy at 13, which is
   the product's chrome scale borrowed for a marketing page: a dashboard is
   scanned at arm's length by someone who already knows the words, and a
   launch page is read by someone who does not. Anything a reader reads as a
   sentence is now 16px, and nothing in the page chrome sits below 14px.

   THE MOCKUP KIT IS OUT OF SCOPE ON PURPOSE, and mk.css pins its own 14px
   baseline for the reason written there: a crop that renders the product's UI
   text larger than the product does has stopped being a crop.

   Leading and tracking are baked into each step's rule below, so it is not
   possible to ship 60px at body leading by forgetting a line-height.
   -------------------------------------------------------------------------- */
:root {
  --t-meta: .75rem;
  --t-small: .875rem;
  --t-body: 1rem;
  --t-title: clamp(1.125rem, 1.5vw, 1.375rem);    /* 18 -> 22 */
  --t-h2: clamp(1.875rem, 3.6vw, 2.625rem);       /* 30 -> 42 */
  --t-display: clamp(2.375rem, 5.4vw, 3.75rem);   /* 38 -> 60 */

  /* One vertical rhythm for every section on every page. Sections differ in
     height only because their artifact differs, never because the padding was
     tuned by hand. 96px is the proportional equivalent, on our 1152px content
     measure, of the 134px the reference pages run on 1300. */
  --band: clamp(64px, 7vw, 96px);

  /* Text never uses the full container; an artifact always may. Measures are
     in CHARACTERS, so they hold at 16px the same way they held at 14: the
     thing that makes a line readable is how many characters sit on it. */
  --m-display: 20ch;
  --m-lead: 44ch;
  --m-prose: 62ch;

  --nav-h: 60px;

  /* ------------------------------------------------------------------------
     THE CONTROL LADDER, and it is the same ladder mk.css runs. A site button
     and a button inside a mockup are the same object, because they are the same
     product. Four heights, two radii, and adding a step is a bug:

       --ctl-h     40px  primary and secondary actions, the hero CTA row
       --ctl-h-sm  36px  the header only, so the whole nav bar fits in 60px
       --ctl-h-xs  32px  tabs, chips, segments, icon buttons
       --ctl-h-2xs 28px  dense only, inside a mockup

       --ctl-r      8px  EVERY control from 28px to 40px
       --r-sm       6px  a 20px label, and nothing else

     8px is the product's own control radius, mirrored here from mk.css --mk-r
     rather than derived from a token, because the point is that the two files
     agree. NOTHING on this site is a capsule: border-radius: 9999px is banned
     outside a status dot and an account avatar.
     ------------------------------------------------------------------------ */
  --ctl-h: var(--control-h);
  --ctl-h-sm: var(--control-h-sm);
  --ctl-h-xs: var(--control-h-xs);
  --ctl-h-2xs: var(--control-h-2xs);
  --ctl-r: 8px;
}


a { color: inherit; text-decoration: none }
img, svg { max-width: 100% }

/* --------------------------------------------------------------------------
   ONE PIXEL, AT EVERY ICON SIZE.

   Every icon on this site is a 1px line, full stop. That is not what a bare
   stroke-width gives you: stroke-width is in viewBox units, so the SAME svg
   drawn at 16px and at 48px paints a 1px line and a 3px line. The 48px feature
   glyphs were measured at 3.0px effective and read as a marker drawing beside
   16px chrome at 1.0px.

   Two declarations, and they have to be separate:

     stroke-width: 1 on the svg. It INHERITS to the shapes, and a CSS rule beats
     a stroke-width presentation attribute, so this one line retires 40 inline
     stroke-width="2"/"1.8" attributes without touching the markup.

     vector-effect on the shapes. It does NOT inherit (measured: set on <svg> it
     computes to `none` on the child), so it cannot go on the svg with the
     width. non-scaling-stroke takes the stroke out of the coordinate system:
     exactly 1 CSS px at 16px, at 48px, at any scale.

   A glyph that needs mass uses a filled shape, not a fat stroke. Drawing dots
   as a zero-length path with a round cap depends on stroke-width being the dot
   diameter, and this rule would reduce it to a speck; use a real <circle>.
   -------------------------------------------------------------------------- */
svg { stroke-width: 1 }
svg :is(path, rect, circle, line, polyline, polygon, ellipse) {
  vector-effect: non-scaling-stroke;
}
/* A package name or a capability id is one long unbreakable token, and one of
   them inside a narrow column is enough to push the whole page sideways. */
p, li, dd, blockquote { overflow-wrap: break-word }
code, pre, kbd, samp { font-family: var(--mono) }
code { overflow-wrap: anywhere }
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1 }
:focus-visible { outline: 2px solid var(--ink-muted); outline-offset: 2px; border-radius: var(--ctl-r) }
/* --ink-muted clears 3:1 on every light surface and fails on the four dark
   ones, so those get a light ring rather than the whole site getting a
   heavier one. */
.footer :focus-visible, .mk-cmd :focus-visible, .mk-term :focus-visible {
  outline-color: rgba(255,255,255,.85);
}

.skip {
  position: absolute; left: -999px; top: 0; z-index: 99;
  padding: var(--sp2) var(--sp3); border-radius: var(--ctl-r);
  background: var(--btn); color: var(--btn-ink);
}
.skip:focus { left: var(--sp3); top: var(--sp3) }

.wrap { max-width: 1200px; margin: 0 auto; padding-inline: var(--sp5) }
/* THE ARTIFACT TRACK IS WIDER THAN THE TEXT TRACK, and that is the point.
   This ran at 1200 for one release, on the reasoning that one track for
   everything is simpler. It is, and it cost the page its product: at 1200 the
   stage holds a 1072px window, the dashboard's chrome plus its table needs
   1195, so the shells came out and bare components went in. The track was sized
   to the artifact that fit rather than the artifact worth showing.

   1440 holds a whole 1232px app shell with stage on both sides at any viewport
   from 1280 up. Prose stays on 1200 (1152 of measure) because a 1440 line is
   ~130 characters and unreadable. One artifact track and one text track is two
   numbers; the bug that mattered was two different ARTIFACT tracks, which made
   two mockup blocks different widths. Every stage on the site is this one. */
.wrap.plate { max-width: 1440px }
/* The row track. Same measure as .plate so a chapter's artifact edge lands on
   the same line as a full-bleed stage's, but it keeps the text rail's side
   padding because a row carries prose and 12px of gutter on a phone is too
   tight for a paragraph. */
.wrap.rows { max-width: 1440px }
/* On a phone the artifact track gives back its side padding. The text rail
   keeps its 24px because prose needs a margin; a stage does not, and those
   24px are the difference between the boot card fitting and being clipped. */
@media (max-width: 560px) { .wrap.plate { padding-inline: var(--sp3) } }

/* ------------------------------- type ------------------------------------- */
/* A section label, not a sentence: mono, lowercase, and quiet enough that the
   heading under it is what the eye lands on. */
.eyebrow {
  margin: 0 0 var(--sp4);
  font-family: var(--mono); font-size: var(--t-meta); font-weight: 500;
  letter-spacing: .02em; color: var(--ink-faint); text-transform: lowercase;
}
.display {
  margin: 0; max-width: var(--m-display);
  font-size: var(--t-display); font-weight: 600;
  letter-spacing: -.03em; line-height: 1.06; text-wrap: balance;
}
.h2 {
  margin: 0; max-width: 22ch;
  font-size: var(--t-h2); font-weight: 600;
  letter-spacing: -.024em; line-height: 1.12; text-wrap: balance;
}
.h3 { margin: 0; font-size: var(--t-title); font-weight: 600; letter-spacing: -.014em; line-height: 1.25 }
/* The one sentence under a display heading. It is a step ABOVE body copy, not
   the same size as it: at 16px under a 60px headline it reads as the first
   paragraph of the page instead of as the headline's other half. */
.lead {
  margin: var(--sp5) 0 0; max-width: var(--m-lead);
  font-size: var(--t-title); font-weight: 400; color: var(--ink-muted); line-height: 1.45;
  letter-spacing: -.008em;
}
/* THE secondary sentence, everywhere: under a section head, beside an
   artifact, inside a card. It is body size, because it is a sentence. */
.sub { margin: var(--sp4) 0 0; max-width: 56ch; font-size: var(--t-body); color: var(--ink-muted); line-height: 1.6 }
.prose { max-width: var(--m-prose) }
.prose p { margin: 0 0 var(--sp5); font-size: var(--t-body); line-height: 1.7 }
.meta { font-family: var(--mono); font-size: var(--t-meta); color: var(--ink-faint) }
.section-head { display: flex; flex-direction: column; gap: var(--sp3); margin-bottom: var(--sp7) }
.section-head .sub { margin: 0 }
.section-head.center { align-items: center; text-align: center }
.section-head.center .h2, .section-head.center .sub { margin-inline: auto }
/* Where a section genuinely needs a preamble it is ONE ROW, not a stack:
   headline left at five columns, one sentence right at six, sharing a top edge.
   An eyebrow over a headline over a paragraph is three blocks of copy, and it
   was sitting above artifacts that already carried their own two lines. */
.section-head.row {
  display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  align-items: start; gap: var(--sp4) var(--sp6);
}
.section-head.row .h2 { max-width: 18ch }
.section-head.row .sub { max-width: 56ch }
@media (max-width: 860px) { .section-head.row { grid-template-columns: minmax(0, 1fr) } }

/* ------------------------------ buttons ----------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp2);
  height: var(--ctl-h); padding: 0 var(--sp4); border-radius: var(--ctl-r);
  font-family: var(--mono); font-size: var(--t-small); font-weight: 500; white-space: nowrap;
  text-transform: lowercase;
  border: 1px solid transparent; cursor: pointer;
  transition: transform var(--fast) var(--ease), background var(--fast) var(--ease),
              border-color var(--fast) var(--ease);
}
.btn:hover svg { transform: translateX(2px) }
.btn.solid { background: var(--btn); color: var(--btn-ink) }
.btn.solid:hover { transform: translateY(-1px) }
.btn.solid:active { transform: translateY(0) }
/* The outline IS the control here, so it clears 3:1 rather than sitting at
   the decorative hairline value the rest of the page uses. */
.btn.ghost { border-color: var(--ink-faint); color: var(--ink) }
.btn.ghost:hover { background: var(--hl-wash, var(--s2)); border-color: var(--ink-faint) }
/* The header runs shorter so the whole bar can hold 60px. */
.btn.sm { height: var(--ctl-h-sm); padding: 0 var(--sp3) }
.cta-row { display: flex; flex-wrap: wrap; gap: var(--sp3); margin-top: var(--sp6) }

/* THE LINE BUTTON, and it is the header "Sign in" button's exact geometry:
   36px tall, 8px radius, 14px label, one hairline, no fill. It exists because
   a card row's action is a control the reader clicks, not a sentence they
   read, and an underlined phrase in a card row is the weakest possible target.
   Use it for every row-level and card-level action on the site.

   The border is --line-2 rather than the header's --ink-faint: inside a card
   the button already sits on its own raised surface, so it needs less ink to
   separate, and hover is what takes it up to the 3:1 the header runs at rest.
   The 36px is deliberate and it is the ONLY height in the ladder that fits a
   card row without crowding its own text; the header's is the same object, so
   the two never disagree. */
.btn.line {
  height: var(--ctl-h-sm); padding: 0 var(--sp3);
  /* --ink-faint, not --line-2: the outline is the whole control, and at the
     decorative hairline value it measured 1.38:1 against the page. */
  background: transparent; border-color: var(--ink-faint); color: var(--ink);
  /* .card is a flex column, so a button in one stretches to the card's width
     unless it says otherwise. It should not: the brief asks for the header's
     button, and the header's button is as wide as its label. .fill opts in
     where a row genuinely wants the whole width. */
  align-self: flex-start;
}
.btn.line.fill { align-self: stretch; width: 100% }
.btn.line:hover { background: var(--hl); border-color: var(--ink-faint) }
.btn.line svg { width: 13px; height: 13px; opacity: .6 }
.btn.line:hover svg { opacity: 1 }
/* Responsive by default: a line button never shrinks its label, so below the
   width where a two-up card row can hold a 36px control beside its text the
   button takes the row. .btn.line.hug opts out where the row genuinely has
   the space (a header, a toolbar). */
@media (max-width: 560px) {
  .card > .btn.line, .cardgrid .btn.line, .hairitem .btn.line { align-self: stretch; width: 100% }
  .card > .btn.line.hug, .cardgrid .btn.line.hug, .hairitem .btn.line.hug { align-self: flex-start; width: fit-content }
  .article-nav .btn.line {
    width: 100%; height: auto; min-height: var(--ctl-h-sm); padding-block: var(--sp2);
    justify-content: space-between; white-space: normal; line-height: 1.35; text-align: left;
  }
}

/* Mid-page links are never buttons. Three hard CTA moments per page (header,
   hero, closer), a line button for a card row's action, and everything else is
   a text link.

   NO UNDERLINE, and no border pretending not to be one. A text link on this
   site is recognisable by three things that are not a rule under the words:
   it is at full --ink where the copy around it is --ink-muted, it carries the
   trailing arrow glyph, and the gap between word and arrow OPENS on hover so
   the whole link visibly reacts. That is a stronger affordance than a 1px line
   and it does not put a horizontal rule through every column of the page. */
.link-arrow {
  display: inline-flex; align-items: center; gap: 6px; width: fit-content;
  font-size: var(--t-body); font-weight: 500; color: var(--ink);
  transition: color var(--fast) var(--ease), gap var(--fast) var(--ease);
}
.link-arrow:hover { color: var(--ink); gap: 10px }
.link-arrow svg { width: 14px; height: 14px }
/* Inside dense chrome (a footer column, a nav menu) the arrow link runs at the
   chrome step, because there it is a chrome item and not a sentence. */
.foot-col .link-arrow, .navdrop-menu .link-arrow { font-size: var(--t-small) }

/* ---------------------------- announcement -------------------------------- */
.announce {
  position: relative; z-index: 41;
  display: flex; align-items: center; justify-content: center; gap: var(--sp2);
  min-height: 40px; padding: var(--sp2) var(--sp5);
  border-bottom: 1px solid var(--line);
  background: var(--s1);
  font-size: var(--t-small); color: var(--ink-muted); text-align: center;
}
.announce b { font-weight: 600; color: var(--ink) }
.announce a { display: inline-flex; align-items: center; gap: 5px; font-weight: 500; color: var(--ink); transition: gap var(--fast) var(--ease) }
/* Same affordance as every other text link on the site: the gap opens, the
   glyph moves. No underline appears on hover, here or anywhere. */
.announce a:hover { gap: 8px }
.announce svg { width: 13px; height: 13px }
.announce-short { display: none }
@media (max-width: 640px) {
  /* One line at 40px, not a wrapped sentence at 77px with an orphaned arrow
     beside it: the bar is the first thing on the page and it should cost one
     row of the fold, not two. It stays at the chrome step while it does that,
     because a 12px sentence is where "everything reads too small" starts. What
     gives way is the WORDING, not the size. */
  .announce { gap: var(--sp2); padding-inline: var(--sp3) }
  .announce .announce-lead, .announce .announce-long { display: none }
  .announce .announce-short { display: inline }
  .announce a { white-space: nowrap }
}
/* At 14px the bar's three parts need 357px and a 375 phone has 351. The part
   that goes is the "Sapiom is live." throat-clear, because the fact and the
   link are the bar, and the reader already knows whose site this is. */
@media (max-width: 480px) { .announce .announce-short b { display: none } }

/* -------------------------------- nav ------------------------------------- */
.nav {
  position: sticky; top: 0; z-index: 40;
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}
.nav-in { display: flex; align-items: center; gap: var(--sp4); height: var(--nav-h) }
.brand { display: inline-flex; align-items: center; min-width: 0; color: var(--ink) }
/* The wordmark IS the name. Never set the S mark beside it, and never set the
   word "Sapiom" in body type as a stand-in: the asset is the lockup. */
.logotype { display: block; height: 19px; width: auto }
.footer .logotype { height: 21px; color: #fff }
/* Left-aligned against the wordmark. A centred nav puts the primary paths
   equidistant from nothing and leaves a hole beside the brand. */
.nav-links { display: flex; align-items: stretch; gap: var(--sp1); align-self: stretch; margin-right: auto }
.nav-links > a, .navdrop > button {
  display: inline-flex; align-items: center; gap: 5px; padding: 0 var(--sp3);
  font-family: var(--mono); font-size: var(--t-small); color: var(--ink-muted);
  text-transform: lowercase;
  background: none; border: 0; border-bottom: 2px solid transparent; margin-bottom: -1px;
  cursor: pointer; transition: color var(--fast), border-color var(--fast);
}
.nav-links > a:hover, .navdrop > button:hover { color: var(--ink) }
.nav-links > a svg.ext { width: 11px; height: 11px; opacity: .5 }
.nav-links a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--ink) }
.navdrop > button[aria-current="page"] { color: var(--ink); border-bottom-color: var(--ink) }
.navdrop { position: relative; display: flex; align-items: stretch }
.navdrop > button svg { width: 12px; height: 12px; transition: transform var(--base) var(--ease) }
.navdrop[data-open="true"] > button { color: var(--ink) }
.navdrop[data-open="true"] > button svg { transform: rotate(180deg) }
/* The Company menu. Secondary audiences live here so the primary nav stays on
   the conversion path. */
.navdrop-menu {
  position: absolute; top: calc(100% + 6px); left: 50%; transform: translateX(-50%) translateY(-4px);
  /* 288, sized to the widest description on ONE line: "Why we exist, and who
     backs it" at --t-small needs 236px of text plus the item's own padding.
     At 232 every description wrapped to a ragged second line and the card read
     as broken. Descriptions are one line each or they get shorter. */
  display: grid; gap: 2px; min-width: 288px; padding: var(--sp2);
  background: var(--s1); border: 1px solid var(--line-2); border-radius: var(--r-xl);
  box-shadow: var(--shadow);
  opacity: 0; visibility: hidden;
  transition: opacity var(--base) var(--ease), transform var(--base) var(--ease), visibility 0s linear var(--base);
}
.navdrop[data-open="true"] .navdrop-menu {
  opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0);
  transition: opacity var(--base) var(--ease), transform var(--base) var(--ease);
}
.navdrop-menu a {
  display: grid; gap: 1px; padding: var(--sp2) var(--sp3); border-radius: var(--ctl-r);
  transition: background var(--fast);
}
.navdrop-menu a:hover { background: var(--active-shade) }
.navdrop-menu b {
  font-family: var(--mono); font-size: var(--t-body); font-weight: 500;
  text-transform: lowercase;
}
.navdrop-menu span { font-size: var(--t-small); color: var(--ink-faint) }
.nav-cta { display: flex; align-items: center; gap: var(--sp2) }
.themebtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--ctl-h-sm); height: var(--ctl-h-sm); border-radius: var(--ctl-r);
  border: 1px solid transparent; background: transparent; color: var(--ink-muted); cursor: pointer;
  transition: background var(--fast), color var(--fast);
}
.themebtn:hover { background: var(--active-shade); color: var(--ink) }
.themebtn svg { width: 17px; height: 17px }
[data-theme="dark"] .ic-moon { display: none }
[data-theme="light"] .ic-sun { display: none }

.menubtn {
  display: none; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  width: var(--ctl-h-sm); height: var(--ctl-h-sm); border: 0; background: transparent; cursor: pointer;
}
.menubtn span {
  display: block; width: 18px; height: 2px; border-radius: 2px; background: var(--ink);
  transition: transform var(--slow) var(--ease), opacity var(--base) var(--ease);
}
html.menu-open .menubtn span:nth-child(1) { transform: translateY(3.5px) rotate(45deg) }
html.menu-open .menubtn span:nth-child(2) { transform: translateY(-3.5px) rotate(-45deg) }
.mnav {
  position: fixed; inset: 0; z-index: 50; overflow-y: auto;
  display: flex; flex-direction: column;
  padding: calc(var(--nav-h) + var(--sp6)) var(--sp5) var(--sp6);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(1.08); backdrop-filter: blur(14px) saturate(1.08);
  opacity: 0; visibility: hidden;
  transition: opacity var(--slow) var(--ease), visibility 0s linear var(--slow);
}
html.menu-open .mnav { opacity: 1; visibility: visible; transition: opacity var(--slow) var(--ease) }
.mnav a {
  display: block; padding: var(--sp4) 0; border-bottom: 1px solid var(--line);
  font-family: var(--mono); font-size: clamp(1.6rem, 6.5vw, 2.1rem); font-weight: 600; letter-spacing: -.025em;
  text-transform: lowercase;
  opacity: 0; transform: translateY(14px);
  transition: opacity .45s var(--ease), transform .45s var(--ease);
  transition-delay: calc(var(--i, 0) * 40ms);
}
.mnav a.sub-item { font-size: 1.05rem; font-weight: 400; color: var(--ink-muted); padding-block: var(--sp3) }
html.menu-open .mnav a { opacity: 1; transform: none }
html.menu-open { overflow: hidden }
@media (max-width: 1040px) {
  .nav-links { display: none }
  .menubtn { display: flex; margin-left: calc(-1 * var(--sp2)) }
  .nav { z-index: 60 }
  .nav-in { gap: var(--sp3) }
  .nav-cta { margin-left: auto }
}
@media (min-width: 1041px) { .mnav { display: none } }
@media (max-width: 560px) { .nav-cta .ghost { display: none } }
@media (max-width: 620px) { .nav-cta .themebtn { display: none } }
@media (max-width: 420px) { .nav-cta { display: none } }

/* ------------------------------ sections ---------------------------------- */
.section { padding-block: var(--band) }
.section + .section { border-top: 1px solid var(--line) }
.section.tight { padding-block: calc(var(--band) * .55) }
/* The FAQ band is the only tinted full-bleed section, and it appears in the
   same slot on every page that has one, so the reader can feel the page ending. */
.band-tint { background: var(--s2); border-top: 1px solid var(--line) }

/* Hero. CENTRED copy, then the artifact, centred inside .wrap rather than
   inside the viewport, so the headline's optical centre is the stage's centre.
   Every element in the stack is capped in characters and centred by the same
   margin-inline: auto, which is the only way a centred hero stays centred once
   a line wraps differently at 1024.

   The rhythm, and it is the whole fix for "no space between the body copy and
   the CTA row": eyebrow +12 headline +16 sub-line +32 CTA row +56 stage. The
   32px before the CTA row is the LARGEST gap in the stack, on purpose. When the
   sub-line and the CTAs are the same distance apart as the headline and the
   sub-line, the row reads as a third line of copy instead of as an action.

   The home hero is the one exception: it carries a compact row of public
   platform counters between the display and the actions. */
.hero { position: relative; overflow: clip; padding-block: clamp(36px, 5vw, 64px) 0 }
.hero-copy { max-width: 780px; margin-inline: auto; text-align: center }
.hero-copy > * { margin-inline: auto }
.hero .display { max-width: 17ch }
.hero .lead { margin-top: var(--sp4); max-width: 44ch }
/* The hero is short on purpose. Product-first means the first pixel of running
   product lands inside the fold, so every gap above it is rationed. */
.hero .cta-row { margin-top: var(--sp6); justify-content: center }
/* A row that survives in the hero markup centres with everything else rather
   than hanging off the left edge under a centred headline. */
.hero-stage { margin-top: clamp(32px, 4vw, 56px) }

/* The developer entry point is a command, not a product screenshot. The card
   stays inside the readable rail and keeps the runnable line left aligned. */
.developer-hero { padding-bottom: var(--band) }
.developer-hero-grid {
  display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  align-items: center; gap: clamp(var(--sp6), 6vw, var(--sp8));
}
.developer-hero .hero-copy {
  width: 100%; max-width: 560px; margin: 0; text-align: left;
}
.developer-hero .hero-copy > * { margin-right: 0; margin-left: 0 }
.developer-hero .display { max-width: 14ch }
.developer-hero .lead { max-width: 48ch }
.developer-command {
  width: min(720px, 100%); margin-top: var(--sp6); padding: var(--sp3);
  border: 1px solid var(--line); border-radius: var(--r-xl);
  background: var(--s1); text-align: left;
}
.developer-hero-art { width: 100%; max-width: 680px; min-width: 0; justify-self: end }
.developer-command-stage { width: 100%; margin: 0 }
.developer-command-stage .developer-command { width: 100%; margin-top: 0 }
.developer-install-stage { width: 100% }
.developer-install-stage .mk-install {
  max-width: none; padding: var(--sp3);
  background: var(--s1); border: 1px solid var(--line-2); border-radius: var(--r-xl);
  box-shadow: var(--shadow-soft);
}
.developer-command-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp3);
  padding: 0 var(--sp1) var(--sp2); color: var(--ink-muted);
}
.developer-command-head strong { color: var(--ink); font-size: var(--t-small); font-weight: 500 }
.developer-command .mk-cmd { margin-top: var(--sp1) }
.developer-command-note {
  margin: var(--sp2) var(--sp1) 0; color: var(--ink-muted);
  font-family: var(--sans); font-size: var(--t-small); line-height: 1.5;
}
.developer-hero .cta-row { justify-content: flex-start; margin-top: var(--sp5) }
@media (max-width: 900px) {
  .developer-hero-grid { grid-template-columns: minmax(0, 1fr); gap: var(--sp7) }
  .developer-hero .hero-copy { max-width: 640px }
  .developer-hero-art { max-width: 720px; justify-self: start }
}

/* Chapter. THE below-fold block, and every block on the site is one of these
   so they are all the same width and the same backdrop.

   Twelve columns on the 1152px content measure, with a 24px gutter, so a column
   is exactly 74px and the arithmetic resolves to the pixel:
     artifact  8 columns = 8x74 + 7x24 = 760px, two thirds
     copy      4 columns = 4x74 + 3x24 = 368px
     760 + 24 + 368 = 1152
   Nothing here is a rounded percentage. Alternation moves which end the copy
   sits at; it never changes either width, because a tour whose mockups are
   three different widths is a tour that reads as four unrelated screenshots.

   Per block the copy is TWO elements: a label or a short headline, and one
   sentence. There is no section preamble above the row and no caption under the
   artifact. Copy above the block repeating the copy beside it is what made the
   whole below-fold read as three paragraphs and a picture. */
/* --------------------------------------------------------------------------
   THE Z. Five numbered rows, one grid, alternating sides, and the artifact never
   changes size.

   This was a 12-column grid with the copy on 4 and the artifact on 8, which made
   the space between them the grid's own 24px gutter. 24px between a paragraph
   and a 760px app window is not a gap, it is a collision: the eye reads the two
   as one object. It also meant the only way to widen the gap was to take columns
   off the artifact, which is the one thing that must not shrink.

   Two explicit tracks instead. The copy is capped at 420 because it holds a
   label, a title at the --t-title step and one sentence; the artifact is 760 and
   stays 760; and the space between them is what is left over, distributed by
   space-between, so at the 1392 artifact measure the gap is 212px and both outer
   edges land on the same track as the full-bleed stages above and below. Widen
   the page and the gap grows; narrow it and the artifact gives up its slack last.
   -------------------------------------------------------------------------- */
.chapter {
  display: grid;
  grid-template-columns: min(420px, 100%) minmax(0, 760px);
  justify-content: center;
  gap: 0 clamp(var(--sp5), 4vw, var(--sp8));
  align-items: center;
}
.chapter + .chapter { margin-top: var(--band) }
.chapter-copy, .chapter-art { grid-row: 1; min-width: 0 }
.chapter-copy { grid-column: 1; display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp3); z-index: 2 }
/* A row's copy may carry a list or a CTA row under its sentence; the list takes
   the column, the buttons do not. */
.chapter-copy .hairlist { align-self: stretch }
.chapter-copy .cta-row { margin-top: 0 }
.chapter-art  { grid-column: 2; z-index: 1 }
/* The alternation, and it is the whole Z. Odd rows read copy then artifact, even
   rows read artifact then copy, so the eye crosses the page on every row instead
   of running down one edge. Column assignment, not `order`, so the DOM keeps the
   copy first and a screen reader always hears the claim before the evidence. */
/* The TRACKS reverse, not just the placement. Moving the copy to column 2 while
   column 2 is still the 760px artifact track handed the paragraph the artifact's
   width and squeezed the app window into the 420 column. */
.chapter.flip { grid-template-columns: minmax(0, 760px) min(420px, 100%) }
.chapter.flip .chapter-copy { grid-column: 2 }
.chapter.flip .chapter-art  { grid-column: 1 }
/* A block heading is the title step, not the 42px a section heading runs. It
   sits beside an artifact, so it is a label for the artifact rather than the
   start of a new chapter of the page, and 42px in a 368px column wraps to four
   lines. */
.chapter-copy .h2 { font-size: var(--t-title); letter-spacing: -.016em; line-height: 1.25; max-width: none }
/* One sentence, and the cap enforces it: 21 words is the longest sentence any
   of the reference blocks carries, and 40 characters of measure is about where
   a second sentence stops fitting. */
.chapter-copy .sub { margin: 0; max-width: 40ch }
.chapter-copy .eyebrow { margin: 0 }
.chapter-num {
  margin: 0; font-family: var(--mono); font-size: var(--t-meta);
  color: var(--ink-faint); letter-spacing: .04em;
}
#how .chapter-num {
  display: flex; flex-direction: column; gap: var(--sp1);
}
#how .chapter-index {
  font-size: calc(var(--t-title) + var(--sp2)); font-weight: 500; line-height: 1.2;
  color: var(--ink-faint); letter-spacing: .04em;
}
#how .chapter-name {
  font-size: calc(var(--t-title) + var(--sp2));
  font-weight: 500; line-height: 1.2;
  color: var(--ink); letter-spacing: -.016em;
}
#how .chapter-statement {
  display: flex; flex-direction: column; gap: 0; max-width: 40ch;
}
#how .chapter-statement .h2,
#how .chapter-statement .sub {
  margin: 0; max-width: none; font-size: var(--t-body); line-height: 1.6;
  letter-spacing: 0;
}
#how .chapter-statement .h2 { font-weight: 600 }
@media (max-width: 560px) {
  #how .chapter-index, #how .chapter-name { font-size: var(--t-title) }
}
@media (max-width: 1000px) {
  /* BOTH selectors, and .chapter.flip is load-bearing: .flip re-declares the
     TRACKS at (0,2,0) specificity, so a bare .chapter reset here loses to it at
     every width and a flipped row kept its two desktop tracks on a phone. That
     rendered its copy one word to a line down a 60px column, which is the worst
     defect this stylesheet has shipped. Same-specificity reset, later in the
     cascade, wins. */
  .chapter, .chapter.flip { grid-template-columns: minmax(0, 1fr); gap: var(--sp5) }
  .chapter-copy, .chapter-art, .chapter.flip .chapter-copy, .chapter.flip .chapter-art { grid-column: 1; grid-row: auto }
  /* Copy first, then the artifact, in both alternations. A flipped block that
     leads with its mockup on a phone puts the answer before the question. */
  .chapter-copy { order: 1 }
  .chapter-art { order: 2 }
  .chapter-copy .sub { max-width: 62ch }
}

/* ----------------------------- logo strip --------------------------------- */
.logostrip { padding-block: calc(var(--band) * .6) }
.logostrip .eyebrow { text-align: center; margin-bottom: var(--sp5) }
.logo-row {
  width: min(960px, 100%); margin-inline: auto;
  display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
  align-items: center; justify-items: center; gap: var(--sp4) var(--sp6);
}
.logo-row img {
  /* min() so the cap never exceeds the cell: a 116px cap in an 85px column
     is what pushed the page sideways at 320. */
  height: 26px; max-width: min(116px, 100%); width: auto; object-fit: contain;
  filter: grayscale(1); opacity: .5; transition: opacity var(--base) var(--ease);
}
.logo-row img:hover { opacity: .85 }
[data-theme="dark"] .logo-row img { filter: grayscale(1) invert(1); opacity: .45 }
[data-theme="dark"] .logo-row img:hover { opacity: .78 }
.logo-row img[src$="polsia.png"] { mix-blend-mode: multiply }
[data-theme="dark"] .logo-row img[src$="polsia.png"] { mix-blend-mode: screen }
@media (max-width: 720px) { .logo-row { grid-template-columns: repeat(3, 1fr) } }
@media (max-width: 420px) { .logo-row img { height: 21px; max-width: min(92px, 100%) } }

/* ------------------------- hairline item lists ---------------------------- */
/* The site's default way to show a set. Open rows over boxed cards. */
.hairlist { display: flex; flex-direction: column }
.hairitem {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: var(--sp4);
  padding-block: var(--sp5); border-top: 1px solid var(--line-2);
}
/* The trailing cell keeps a reserved track at every width unless it stacks, and
   on a phone that leaves the prose a 27-character measure. */
@media (max-width: 640px) {
  .hairitem { grid-template-columns: minmax(0, 1fr); gap: var(--sp3) }
  .hairitem .meta, .hairitem .link-arrow { justify-self: start; padding-top: 0 }
}
.hairlist > .hairitem:last-child { border-bottom: 1px solid var(--line-2) }
.hairitem h3 { margin: 0; font-size: var(--t-title); font-weight: 600; letter-spacing: -.014em; line-height: 1.25 }
.hairitem p { margin: var(--sp2) 0 0; max-width: 62ch; font-size: var(--t-body); color: var(--ink-muted); line-height: 1.6 }
/* The trailing cell of a hairline row is a VALUE, not a timestamp in a dense
   table: a price, a date, a role's location. It runs at the chrome step rather
   than the meta step, because 12px beside 16px copy reads as a footnote and
   the whole reason the row has two columns is that both cells matter. */
.hairitem .meta { padding-top: 3px; font-size: var(--t-small) }
@media (min-width: 641px) { .hairitem .meta { white-space: nowrap } }
.hairitem .lede { display: flex; align-items: baseline; gap: var(--sp3); min-width: 0 }
.hairitem .ord { font-family: var(--mono); font-size: var(--t-meta); color: var(--ink-faint) }

/* Column set. Three or four unboxed columns of small facts. */
.colset { display: grid; grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr)); gap: var(--sp7) var(--sp6) }
.colset > div { display: flex; flex-direction: column; gap: var(--sp3); border-top: 1px solid var(--line-2); padding-top: var(--sp5) }
.colset h3 { margin: 0; font-size: var(--t-title); font-weight: 600; letter-spacing: -.014em; line-height: 1.25 }
.colset p { margin: 0; max-width: 40ch; font-size: var(--t-body); color: var(--ink-muted); line-height: 1.6 }
.colset .link-arrow { margin-top: auto }
@media (max-width: 1000px) { .colset { grid-template-columns: repeat(2, minmax(0, 1fr)) } }
@media (max-width: 640px) { .colset { grid-template-columns: minmax(0, 1fr); gap: var(--sp5) } }

/* Card. Ring-first elevation: a hairline ring and a flat surface. Shadow is
   reserved for the mockup windows, which is what keeps them reading as objects
   floating above the page rather than as more page. */
.card {
  display: flex; flex-direction: column; gap: var(--sp3);
  padding: clamp(24px, 3vw, 32px);
  background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-xl);
}
/* The card owns its rhythm through gap, so a child that carries its own top
   margin would double the space. This is the rule every card grid needs and
   nobody remembers to write. */
.card > .sub, .card > p { margin: 0 }
/* --------------------------------------------------------------------------
   ONE SET OF BASELINES ACROSS A ROW OF CARDS, and this is a real defect being
   fixed rather than a preference.

   Measured across the site at 1440, before this: in a three-card row the titles
   started at 97, 122.6 and 149.6 from the card's top, and on /company/press-kit/
   at 97, 148.2 and 173. A 76px spread. The glyph was top-anchored and the rest
   of the stack was pushed to the BOTTOM by `margin-bottom: auto` on the glyph,
   so the card with the longest body pushed its own title highest and every card
   in the row disagreed with its neighbours.

   Two rules fix it and both are needed:

     min-height on the title, at TWO LINES. Top-anchoring alone lines the titles
     up but not the bodies: a one-line title next to a two-line one starts its
     body a line higher. 2.5em is two lines at the 1.25 line-height set below,
     and it is in em so it tracks the clamp on --t-title.

     margin-top: auto on the action. It is the ONLY thing that should float, and
     it floats to the bottom, so every card's button sits on one line.

   A card grid already stretches its cards to one height, so with the top of the
   stack pinned and the action pinned to the bottom, only the body can differ,
   which is the one thing that is allowed to.
   -------------------------------------------------------------------------- */
.card h3 {
  margin: 0; font-size: var(--t-title); font-weight: 600;
  letter-spacing: -.014em; line-height: 1.25;
  min-height: 2.5em;         /* two lines, so the body starts on one line */
  text-wrap: balance;
}
/* The reservation exists to line a title up with the ones BESIDE it. At the
   one-column step there is nothing beside it, and a title that needs three lines
   at 320px would be the only one holding the row: measured on /company/, a
   three-line title started its body 22px below its two-line neighbour's while
   the reservation stubbornly held two. With one card per row the alignment has no
   reader, so the reservation goes and the card closes up. */
@media (max-width: 620px) { .card h3 { min-height: 0 } }
.card p { font-size: var(--t-body); color: var(--ink-muted); line-height: 1.6 }
.card .link-arrow, .card > .btn { margin-top: auto }
.cardgrid { display: grid; grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr)); gap: var(--sp4) }
/* Two steps down, not one. A three-up grid that goes straight to one column at
   900 gives a tablet a stack of 720px-wide cards holding one sentence each,
   which is the widest a card ever gets and the least it ever says. A grid with
   --cols: 2 is unaffected, because it is already at the two-up step. */
@media (max-width: 960px) {
  .cardgrid { grid-template-columns: repeat(2, minmax(0, 1fr)) }
  /* An odd peer count at the two-up step leaves the last card beside an empty
     cell. It takes the row instead: a wide card is a card, a hole is a defect. */
  .cardgrid > :last-child:nth-child(odd) { grid-column: 1 / -1 }
}
@media (max-width: 620px) { .cardgrid { grid-template-columns: minmax(0, 1fr) } }

/* --------------------------------------------------------------------------
   THE TEXTURE CARD, .card.tex. The answer to an empty column, and it is a
   card first: same fill, same hairline, same radius, same padding as every
   other .card, so a texture card in a grid of three is the third card and not
   a different kind of object.

   What it adds is two things and no more:

     1. ONE big line icon, --tex-icon (48px), 1.5px stroke, at --ink. It is
        48px because at 24 it reads as a bullet beside the heading and at 72 it
        reads as an illustration; 48 is the size at which a single stroke glyph
        is the card's subject. The page supplies the glyph inline, and it has
        to MEAN the sentence under it. A gear over "what a run costs" is worse
        than no glyph, because the reader stops to work out the mismatch.

     2. A guilloche wash bled off the bottom-right corner. SIX motifs, in
        ../art/, and the card picks the one that means what it says: an
        enclosure figure over a card about limits, a moire of orbits over one
        about fleets, a woven band over one about two parties meeting. They are
        generated by ../scripts/make-guilloche.mjs from the real curve families
        (hypotrochoid, epitrochoid, orbit ring, woven carrier, nested pass), so
        they are visibly one system without being one drawing.

        The first version was a single 83 KB rosette on all 32 cards, drawn
        twice on top of itself to darken it. Two problems: every card carried
        the identical corner, and stacking a drawing on itself does not make it
        more legible, it makes it heavier, which is how line-art becomes a
        smudge. Now: ONE layer, one alpha knob (--tex-ink), and the drawing
        earns its legibility by being sparser.

        Dark inverts the strokes to near-white at the same alpha, which is the
        only way one monochrome asset serves both themes without a second file.

   The card is NEVER just the texture. It carries the same title and sentence
   its neighbours carry; the wash is the third thing the reader notices, not
   the first. A card whose only content is art is the placeholder this replaces.
   -------------------------------------------------------------------------- */
/* Dark needs a touch more alpha for the same read: an inverted stroke is
   lightening a dark surface, and the eye is less sensitive to that direction. */
:root, [data-theme="light"] { --tex-invert: 0; --tex-ink: .085 }
[data-theme="dark"]        { --tex-invert: 1; --tex-ink: .11 }
:root {
  --tex-icon: 48px;
  /* The wash is sized and bled in PIXELS, not percentages. A percentage of the
     card resolves against the card's width for `right` and its height for
     `bottom`, so the same rule drew a 484px spirograph in a 2-up grid and a
     328px one in a 3-up, overflowing the top edge in the wider case. In px the
     motif is the same object at every column count and only its position in
     the corner changes. 288 over a 248 floor leaves 32px of clear card above
     it, which is what keeps it a corner motif rather than a background. */
  --tex-wash: 288px;
  --tex-bleed-x: -40px;
  --tex-bleed-y: -72px;
}
.card.tex {
  position: relative; isolation: isolate; overflow: hidden;
  /* A floor, so a grid of texture cards and content cards keeps one row
     height when the copy is short. 248px is a 48px glyph, a title, a
     two-line sentence and the card's own padding, with nothing to spare. */
  min-height: 248px;
  gap: var(--sp4);
}
/* The motif, one per meaning. A card with no modifier gets the enclosure figure,
   which is the safest default: it is the one that reads as "a boundary", and
   almost every card on this site is about something being held. */
.card.tex           { --tex-art: url("../art/guilloche-rosette.svg") }
.card.tex-orbit     { --tex-art: url("../art/guilloche-orbit.svg") }
.card.tex-weave     { --tex-art: url("../art/guilloche-weave.svg") }
.card.tex-reticule  { --tex-art: url("../art/guilloche-reticule.svg") }
.card.tex-growth    { --tex-art: url("../art/guilloche-growth.svg") }
.card.tex-loop      { --tex-art: url("../art/guilloche-loop.svg") }
.card.tex::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  right: var(--tex-bleed-x); bottom: var(--tex-bleed-y);
  width: var(--tex-wash); height: var(--tex-wash);
  background-image: var(--tex-art);
  background-size: 100% 100%;
  background-repeat: no-repeat;
  opacity: var(--tex-ink);
  /* invert(0) is a no-op in light. In dark it flips the near-black strokes to
     near-white and leaves the transparent field transparent, because a filter
     inverts channels and not alpha. */
  filter: invert(var(--tex-invert));
}
/* The glyph. --ink-muted, not --ink: it is the card's subject but the title is
   still the card's first line, and a 48px full-ink glyph outranks it. */
.card.tex > svg, .card.tex .glyph > svg {
  width: var(--tex-icon); height: var(--tex-icon); color: var(--ink-muted);
}
/* The monogram glyph is a filled mark, not a stroke icon, and its box is 12x14:
   at the shared 48px slot it renders 41px wide, which reads a step heavier than
   the line glyphs beside it, so it runs slightly smaller. */
.card.tex .glyph.mark svg { width: 34px; height: 40px }
/* The glyph takes the top of the card and NOTHING here floats. `margin-bottom:
   auto` used to live on this rule, which bottom-anchored the title and the body
   and is exactly what made a row of titles disagree by up to 76px. The slack in
   a stretched card belongs at the bottom, under the copy, where the action sits
   on it; it does not belong between the glyph and the title. */
.card.tex .glyph { display: flex; align-items: center; height: var(--tex-icon); flex: 0 0 auto }
@media (max-width: 560px) {
  /* On a phone the card is the full measure and shorter, so the motif comes
     down with it rather than filling the card and reading as wallpaper. */
  .card.tex { min-height: 208px; --tex-icon: 40px; --tex-wash: 224px; --tex-bleed-y: -56px }
}

/* --------------------------------------------------------------------------
   THE FILTER RAIL, .railwrap > .filterrail + the content column. Geometry
   only: the page supplies the search field's label and the rows.

   Copied off /webapp/templates rather than invented, because the product
   already ships this exact column and a marketing page that redraws it wider
   or rounder is a marketing page contradicting the app it sells.

   Measured off the product: column 216px, search field 216 x 42, r14, one
   --line-2 hairline, a 16px glyph then the input; 16px of air; then rows of
   216 x 36 at r10, label left, count right in mono, 2px apart. The one
   deviation is the field's height: the product's 42px is not a step on this
   site's control ladder and adding one would be the bug the ladder exists to
   prevent, so it runs at --ctl-h (40) and the 2px goes nowhere anybody can
   see. Everything else is the product's number.
   -------------------------------------------------------------------------- */
.railwrap {
  display: grid; grid-template-columns: 216px minmax(0, 1fr);
  gap: var(--sp6); align-items: start;
}
.filterrail {
  display: flex; flex-direction: column; gap: var(--sp4);
  position: sticky; top: calc(var(--nav-h) + var(--sp5));
}
.filtersearch {
  display: flex; align-items: center; gap: var(--sp2);
  width: 100%; height: var(--ctl-h); padding: 0 var(--sp3);
  background: var(--s1); border: 1px solid var(--ink-faint); border-radius: var(--r-lg);
  transition: border-color var(--fast) var(--ease);
}
.filtersearch:focus-within {
  border-color: var(--ring); outline: 2px solid var(--ring); outline-offset: 2px;
}
.filtersearch > svg { width: 16px; height: 16px; color: var(--ink-faint) }
/* Longhands, never the font shorthand: `font: 400 var(--t-small)/1.5 var(--sans)`
   silently resets font-size when the custom property carries no unit-bearing
   size the shorthand can parse. */
.filtersearch input {
  flex: 1; min-width: 0; margin: 0; padding: 0;
  border: 0; background: none; outline: none;
  font-family: var(--sans); font-size: var(--t-body); line-height: 1.5; color: var(--ink);
}
.filtersearch input::placeholder { color: var(--ink-muted) }
.filterlist { display: flex; flex-direction: column; gap: var(--sp0) }
.filterhead {
  margin: 0; padding-inline: var(--sp3);
  font-family: var(--mono); font-size: var(--t-meta); font-weight: 500;
  letter-spacing: .02em; color: var(--ink-faint); text-transform: lowercase;
}
.filterrow {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp3);
  width: 100%; height: var(--ctl-h-sm); padding: 0 var(--sp3);
  border: 0; background: transparent; border-radius: var(--r);
  font-family: var(--sans); font-size: var(--t-small); color: var(--ink-muted);
  text-align: left; cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.filterrow .n { font-family: var(--mono); font-size: var(--t-meta); color: var(--ink-faint); font-variant-numeric: tabular-nums }
.filterrow:hover { background: var(--hl); color: var(--ink) }
/* Selected is a wash and full ink, never a border and never a fill with a
   radius the rest of the column does not have. */
.filterrow[aria-pressed="true"] { background: var(--hl-2); color: var(--ink) }
.filterrow[aria-pressed="true"] .n { color: var(--ink-muted) }
@media (max-width: 900px) {
  /* The rail unstacks into a horizontal scroller above the content: a 216px
     column beside a 320px one is two narrow columns and no reading measure.
     The search field keeps the full width because a search field the reader
     has to scroll to is a search field they will not find. */
  .railwrap { grid-template-columns: minmax(0, 1fr); gap: var(--sp5) }
  .filterrail { position: static; gap: var(--sp3) }
  .filterhead { display: none }
  .filterlist {
    flex-direction: row; gap: var(--sp1);
    overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
    -webkit-mask-image: linear-gradient(to right, black 0, black calc(100% - 32px), transparent);
            mask-image: linear-gradient(to right, black 0, black calc(100% - 32px), transparent);
  }
  .filterlist::-webkit-scrollbar { display: none }
  .filterrow { flex: 0 0 auto; width: auto; gap: var(--sp2); border: 1px solid var(--line) }
  .filterrow[aria-pressed="true"] { border-color: var(--ink-faint) }
}

/* --------------------------------- FAQ ------------------------------------ */
/* The FAQ is one of the four sections the rhythm CENTRES: it has no artifact
   and the questions are the whole content. So the column is centred in the
   measure, not parked against the left edge, which is what made the band read
   as misaligned. The rows inside it stay left-aligned: a centred question over
   a centred answer is a poster, not a list. */
.faq { max-width: 720px; margin-inline: auto; text-align: left }
/* And the HEAD centres with it, from the presence of the list rather than from
   a class the markup has to remember. A 720px column of questions centred in
   1152px under a heading hard against the left margin is worse than either
   alignment on its own: the reader sees two different left edges 300px apart
   and reads it as a layout bug, which is exactly what it was. :has() is the
   right tool because the rhythm follows the CONTENT, and a section that
   contains a .faq-list is by definition one of the centred four. */
.section:has(.faq-list) > .wrap > .section-head { align-items: center; text-align: center }
.section:has(.faq-list) > .wrap > .section-head > * { margin-inline: auto }
.section:has(.faq-list) { padding-block: calc(var(--band) * .72) }
.section:has(.faq-list) > .wrap > .section-head { margin-bottom: var(--sp5) }
.faq-item { border-top: 1px solid var(--line-2) }
.faq-list > .faq-item:last-child { border-bottom: 1px solid var(--line-2) }
.faq-q {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp4);
  width: 100%; min-height: 56px; padding: var(--sp3) 0;
  background: none; border: 0; cursor: pointer; text-align: left;
  font-family: var(--sans); font-size: var(--t-body); font-weight: 500; color: var(--ink);
}
.faq-q svg { flex: 0 0 auto; width: 16px; height: 16px; color: var(--ink-faint); transition: transform var(--base) var(--ease) }
.faq-item[data-open="true"] .faq-q svg { transform: rotate(45deg) }
.faq-a { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--slow) var(--ease) }
.faq-item[data-open="true"] .faq-a { grid-template-rows: 1fr }
/* grid-template-rows: 0fr hides the answer visually but leaves it readable to
   a screen reader, so aria-expanded="false" would be a lie. visibility takes
   it out of the tree, and the delay is on the close side only so the collapse
   still animates. */
.faq-a > div { overflow: hidden; visibility: hidden; transition: visibility 0s linear var(--slow) }
.faq-item[data-open="true"] .faq-a > div { visibility: visible; transition: visibility 0s }
.faq-a p { margin: 0 0 var(--sp3); max-width: 62ch; font-size: var(--t-body); color: var(--ink-muted); line-height: 1.65 }

/* -------------------------------- closer ---------------------------------- */
.closer { padding-block: var(--band) calc(var(--band) * 1.5); text-align: center }
.closer .display { margin-inline: auto; max-width: 16ch }
.closer .lead { margin-inline: auto; max-width: 44ch }
.closer .cta-row { justify-content: center }

/* ------------------------------- footer ----------------------------------- */
.footer { position: relative; overflow: hidden; background: #111114; color: #fff }
/* The watermark is the wordmark itself, not the name re-set in body type. Two
   letterforms for one name a few hundred pixels apart is the tell that the
   lockup is decoration rather than an asset. */
.footer-mark {
  position: absolute; left: 50%; bottom: 0; z-index: 1;
  /* 26% of the MARK's own height, not of the footer's: the wordmark's tittles
     end 24% down the glyph box, and on a short footer a percentage bottom
     offset let the two dots float alone above the clipped letter bodies, a
     detached grey square at the seam. Translating by the mark's own height
     puts the cut at the letter bodies at every footer height. */
  transform: translate(-50%, 26%);
  width: 108%; height: auto; color: rgba(255,255,255,.035); pointer-events: none;
}
.foot-grid {
  position: relative; z-index: 2;
  display: grid; grid-template-columns: 1.3fr 2.6fr; gap: clamp(32px, 5vw, 56px);
  padding-block: clamp(56px, 8vh, 88px) clamp(28px, 4vh, 44px);
}
.foot-brand p { margin: var(--sp3) 0 0; max-width: 30ch; font-size: var(--t-body); color: rgba(255,255,255,.56); line-height: 1.6 }
.foot-cols { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--sp5) }
.foot-col h3 { margin: 0 0 var(--sp3); font-size: var(--t-small); font-weight: 500; color: rgba(255,255,255,.5) }
/* flex, not inline-flex: two short labels were sharing a line and reading as
   one phrase ("Terms Privacy DPA"). */
.foot-col a {
  display: flex; width: fit-content; align-items: center; gap: 5px; padding: 4px 0;
  font-size: var(--t-small); color: rgba(255,255,255,.72); transition: color var(--fast);
}
.foot-col a:hover { color: #fff }
/* External links carry the outbound glyph and internal ones do not. The
   distinction holds site-wide so the arrow means one thing. */
.foot-col a svg, .link-arrow svg.ext { width: 11px; height: 11px; opacity: .55 }
.foot-base {
  position: relative; z-index: 2;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--sp3);
  padding-block: var(--sp4); border-top: 1px solid rgba(255,255,255,.1);
  font-size: var(--t-small); color: rgba(255,255,255,.56);
}
/* The glyph stays 18px; the hit area is padded out to 44 and the gap grows to
   keep three 44px targets from overlapping. */
.foot-socials { display: flex; gap: var(--sp2); margin: calc(-1 * var(--sp3)) }
.foot-socials a { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; color: rgba(255,255,255,.55); transition: color var(--fast), transform var(--base) var(--ease) }
.foot-socials a:hover { color: #fff; transform: translateY(-2px) }
.foot-socials svg { width: 18px; height: 18px }
@media (max-width: 900px) { .foot-cols { grid-template-columns: repeat(3, 1fr) } }
@media (max-width: 760px) { .foot-grid { grid-template-columns: minmax(0, 1fr); gap: var(--sp6) } .footer-mark { width: 150% } }
@media (max-width: 480px) { .foot-cols { grid-template-columns: repeat(2, 1fr); gap: var(--sp5) } }

/* ------------------------------- motion ----------------------------------- */
/* Each section enters once. Replaying the entrance on every reverse scroll
   made long pages feel busy and kept the observer doing unnecessary work. */
.reveal {
  opacity: 0; transform: translateY(14px);
  transition: opacity .5s var(--ease), transform .55s cubic-bezier(.22, 1, .36, 1);
}
.reveal.in { opacity: 1; transform: none }

/* A single visibility-gated controller writes this individual transform
   property. It composes with component transforms, never changes layout, and
   is capped at four pixels per visual layer. */
[data-motion-depth] { translate: 0 var(--motion-y, 0px) }
[data-motion-depth].motion-live { will-change: translate }
@media (max-width: 767px) {
  [data-motion-depth] { translate: none !important; will-change: auto !important }
}

/* One reduced-motion block for the whole sheet. A component-by-component guard
   is a guard that gets forgotten on the next component. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .reveal { opacity: 1; transform: none }
  [data-motion-depth] { translate: none !important; will-change: auto !important }
}
