/* NIX — the parts every page is built from
   ══════════════════════════════════════════════════════════════════════════
   A part is here when more than one page wears it. That is the whole rule,
   and it is checked rather than remembered: the split that made these files
   compared every selector against the markup of all six pages, and anything
   only one page could match went to that page's sheet.

   In the order they appear:

     the theme toggle          the dot that fills when the lights go out
     the preloader             /services and /products, the sheet that wipes up
     the header                the bar, its mark, its trigger
     the gate                  Assisted | Manual, once ever, over whatever page
     assisted mode             the guide's own screen and its orb
     the navigation switch     the two cards the gate offers
     buttons                   .btn and its three faces
     the FAQ                   a disclosure, not a card
     the reveal                the rise every section arrives on
     the foot                  the one-line footer and the language switch
     the page reveal           what comes up as the preloader lifts
     the menu                  the trigger, the overlay, the cards in it
     the guide switch          Assisted | Manual again, as a setting
     the tag bar               the pill of links: tags on the blog, sections on
                               the two story pages
     the card artwork          the orbit, the shot, the shelf, the store and
                               the drop that live inside a .menu-card
     the four-column foot      the site's real footer
     the story pages           the hero /products and /services share

   The artwork block is the one that looks page-specific and is not: those
   miniatures appear on the index, in the menu overlays and on the store.
   ══════════════════════════════════════════════════════════════════════════ */

/* theme toggle: an outline dot that fills when the lights go out.
   18, and it stays 18: this is a picture of a state, and it sits opposite a
   16px icon in the account menu — at 24 it was half again louder than the row
   it belongs to. Its REACH is the 24px box below, laid over it. */
.theme-toggle {
  position: relative;
  width: 18px;
  height: 18px;
  padding: 0;
  border: 1.5px solid var(--nix-text);
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
}

html[data-theme="dark"] .theme-toggle {
  background: var(--nix-text);
}

/* THE REACH IS A BOX AROUND THE DRAWING, NOT A BIGGER DRAWING. min-width
   clamps the used width — it does not add room around a glyph, it inflates
   it — so the last attempt at this grew the dot to 24 on every page while the
   comment above it argued for 18. A transparent pseudo-element is the only
   way to have both: the ring is still an 18px circle and the thing a finger
   has to find is 24 square.

   On the trigger the ring IS the target and is sized for it; this is for the
   controls that are pictures of a state. .lang__opt is a text button whose
   line box already clears 24, and .guide__switch is parked behind gate.js's
   SHOW_SWITCH flag — so this names the one control that needed it. */
.theme-toggle::after {
  content: "";
  position: absolute;
  inset: -3px;
}

.theme-toggle:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* ---- Preloader --------------------------------------------------------
   The brand statement assembles itself, holds, then the whole sheet
   wipes upward revealing the page. */
.preloader {
  position: fixed;
  inset: 0;
  z-index: var(--z-veil);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--nix-paper);
  padding: var(--space-8) var(--space-6);
}

.preloader__tagline {
  font-size: clamp(17px, 2.2vw, 22px);
  font-weight: 300; /* Light */
  line-height: 1.45;
  letter-spacing: 0.005em;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 700ms var(--ease-out-expo) 800ms,
              transform 700ms var(--ease-out-expo) 800ms;
}

.preloader--play .preloader__tagline {
  opacity: 1;
  transform: none;
}

/* exit: the sheet wipes upward */
.preloader--exit {
  transform: translateY(-100%);
  transition: transform 800ms var(--ease-in-out);
}

/* ---- Header ---------------------------------------------------------- */

/* The stack, and it is load-bearing in both directions:
     menu overlay 80  — the header's own sheet, so it opens BENEATH the mark
     site-head    85
     preloader    88  — must cover the bar, or the menu button shows during the intro
     paper grain  90  — must wash OVER the bar, or the bar reads a shade darker
                        than the page it sits on (the grain lightens everything
                        under it, and an opaque bar above it gets none)
     app wall 95 / app doc 96 — the viewer covers everything
   Raise the bar past 88 and the preloader leaks. Past 90 and the bar goes dark.
   Both happened. */
.site-head {
  position: relative;
  z-index: var(--z-head);
  padding: var(--space-3) var(--space-6);
  display: flex;
  align-items: center;
  justify-content: center;
}

.site-head__brand {
  display: inline-block;
  line-height: 0;
}

.site-head__logo {
  height: 34px;
  width: auto;
  display: block;
}

/* ---- The gate ---------------------------------------------------------
   The chooser is not a section of any one page. gate.js raises it as an
   overlay on whichever page the visitor lands on first, once ever, and the
   stored choice keeps it down from then on.

   78 is deliberate, and the stack it belongs to is documented at .site-head:
   under the menu overlay (80) and under the bar (85), so the mark and the
   menu button stay live while the gate is up and opening the menu covers it
   instead of fighting it. Opaque, because whatever is behind it — a landing
   still hidden, or AB01's whole story — is not what is being offered yet. */
.nav-gate {
  position: fixed;
  inset: 0;
  z-index: var(--z-gate);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6) var(--space-3);
  /* two cards stacked on a short phone are taller than the viewport */
  overflow-y: auto;
  background: var(--nix-paper);
  opacity: 0;
  transition: opacity 420ms var(--ease-out-expo);
}

.nav-gate.is-up {
  opacity: 1;
}

/* after .is-up, so leaving wins over having arrived */
.nav-gate.is-out {
  opacity: 0;
  pointer-events: none;
}

/* Nothing behind the sheet moves. The overlay is not inside the scroller, so
   a wheel over it finds no scrollable ancestor anyway — this is for the
   keyboard and for anything that reaches the scroller some other way. */
body.is-gated .page {
  overflow: hidden;
}

/* ---- Assisted mode ----------------------------------------------------
   The guide's own screen: a line, a few ways in, and the orb. 79 sits it
   one step above the gate that opened it and still under the menu and the
   bar — the stack is documented at .site-head. */
.assist {
  position: fixed;
  inset: 0;
  z-index: var(--z-assist);
  overflow-y: auto;
  /* Not a flat ground. The wash is centred on the orb, so the sphere sits in
     its own light instead of being pasted onto the page. */
  background:
    radial-gradient(90% 70% at 72% 42%, rgba(255, 255, 255, 0.10), transparent 70%),
    var(--nix-paper);
  opacity: 0;
  transition: opacity 420ms var(--ease-out-expo);
}

.assist.is-up { opacity: 1; }

/* after .is-up, so leaving wins over having arrived */
.assist.is-out {
  opacity: 0;
  pointer-events: none;
}

body.is-assisting .page {
  overflow: hidden;
}

.assist__grid {
  min-height: 100%;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: var(--space-6);
  padding: var(--space-8) var(--space-6);
}

.assist__copy {
  max-width: 30rem;
  justify-self: end;
  /* opacity and the slide are written by layout() in assist.js, off the same
     number that moves the orb — a CSS transition here would race it. */
  will-change: opacity, transform;
}

.assist__ask {
  font-size: clamp(28px, 3.4vw, 44px);
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: -0.015em;
  color: var(--nix-text);
  margin-bottom: var(--space-6);
}

/* A caret, because the line is a question waiting on an answer — and because
   this is the spot the real input takes when there is one. */
.assist__caret {
  display: inline-block;
  width: 0.42em;
  height: 0.92em;
  margin-left: 0.18em;
  vertical-align: -0.08em;
  background: var(--nix-text);
  animation: assist-blink 1.15s steps(1, end) infinite;
}

@keyframes assist-blink {
  0%, 49%   { opacity: 1; }
  50%, 100% { opacity: 0; }
}

.assist__ways {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}

/* Pills, on the site's own card recipe: surface plus layered shadow, never a
   border. */
.assist__way {
  max-width: 100%;
  padding: 11px 18px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--nix-surface);
  box-shadow: 0 1px 2px rgba(26, 26, 26, 0.06),
              0 12px 30px -14px rgba(26, 26, 26, 0.18);
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 300;
  line-height: 1.4;
  color: var(--nix-ink);
  text-align: left;
  text-shadow: none;
  cursor: pointer;
  transition: transform var(--dur-3) var(--ease-out-expo),
              box-shadow var(--dur-3) ease;
}

.assist__way:hover {
  transform: translateY(-2px);
  box-shadow: 0 2px 4px rgba(26, 26, 26, 0.06),
              0 20px 44px -16px rgba(26, 26, 26, 0.24);
}

.assist__way:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* A placeholder, and nothing else. The orb is positioned by hand against the
   whole overlay (see layout() in assist.js) so it can leave this cell and take
   half the screen when it takes a form — a box that is sometimes in the grid
   and sometimes out of it cannot animate between the two. This keeps the
   column the width it would have been. */
.assist__stage {
  width: min(46svh, 30vw, 460px);
  height: min(46svh, 30vw, 460px);
  justify-self: center;
}

.assist__orb {
  position: absolute;
  left: 0;
  top: 0;
}

/* The circle is cut twice on purpose. clip-path gives the shape; the radial
   mask, stopped half a pixel short, eats the staircase clip-path leaves on
   its own — the same pairing the reference uses, and the reason its edge
   looks blown rather than cut. */
.assist__orb-clip {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  overflow: hidden;
  clip-path: circle(50% at 50% 50%);
  -webkit-mask-image: radial-gradient(closest-side, #000 calc(100% - 0.5px), transparent);
  mask-image: radial-gradient(closest-side, #000 calc(100% - 0.5px), transparent);
  /* Stand-in for a browser with no WebGL: a lit ball, near enough to keep the
     screen composed. The canvas covers it the moment three is running. */
  background:
    radial-gradient(58% 48% at 34% 28%, rgba(255, 255, 255, 0.95), transparent 62%),
    radial-gradient(70% 70% at 68% 76%, rgba(150, 170, 255, 0.35), transparent 70%),
    radial-gradient(100% 100% at 50% 50%, var(--nix-surface), var(--nix-rule));
}

.assist__canvas {
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 700ms var(--ease-out-expo);
}

.assist__canvas.is-live { opacity: 1; }

/* Once WebGL is painting, the stand-in and the painted rim are not a fallback
   any more — they are a grey halo in the ring of canvas the sphere does not
   fill, and a second edge outside the real one. */
.assist__orb-clip.is-live { background: none; }

.assist__orb-rim.is-off { opacity: 0; }

/* Inset only. An outer ring would draw a circle around the sphere; these draw
   the thickness of its wall. */
.assist__orb-rim {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0.35;
  box-shadow:
    rgba(255, 255, 255, 0.7) 0 1px 1px inset,
    rgba(255, 255, 255, 0.45) 0 -1px 1px inset,
    rgba(255, 255, 255, 0.22) 0 0 0 1px inset,
    rgba(255, 255, 255, 0.18) 0 0 13px inset;
}

/* The way out is a button, and it looks like one: the same white pill the
   index gives the way in, so the two doors of this screen are the same kind
   of thing. It was a grey line of type at half opacity, which is a caption
   that happens to be clickable. */
.assist__out {
  position: absolute;
  left: var(--space-6);
  bottom: var(--space-6);
  width: 42px;
  height: 42px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--nix-text);
  color: var(--nix-paper);
  cursor: pointer;
  transition: opacity var(--dur-3) ease;
}

.assist__out svg { width: 18px; height: 18px; display: block; }

.assist__out:hover { opacity: 0.86; }

/* This is the only way out of a full-screen overlay, so it is the last control
   that should be findable only by pointer. */
.assist__out:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* The orb leads on a phone: it is the thing being introduced, and a column
   that starts with a question and no face is just a form. */
@media (max-width: 900px) {
  .assist__grid {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    gap: var(--space-4);
    padding: var(--space-8) var(--space-3) 96px;
  }
  .assist__copy {
    justify-self: center;
    order: 2;
    text-align: center;
  }
  .assist__stage { order: 1; }
  .assist__stage { width: min(58vw, 300px); height: min(58vw, 300px); }
  .assist__ways { align-items: center; }
  .assist__out {
    left: 50%;
    transform: translateX(-50%);
    bottom: var(--space-3);
  }
}

@media (prefers-reduced-motion: reduce) {
  .assist { transition: none; }
  .assist__caret { animation: none; }
  .assist__canvas { transition: none; }
  .assist__way { transition: none; }
}

/* ---- Navigation mode switch ------------------------------------------
   A Braun slide switch: two positions, one thumb, an LED that lights
   up when assistance is on. */
.nav-mode {
  text-align: center;
}

.nav-mode__sub {
  font-size: clamp(26px, 4vw, 40px);
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--nix-text);
  margin-bottom: var(--space-6);
}

.nav-mode__switch {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
  width: min(760px, 92vw);
  margin: 0 auto;
}

/* Two quiet cards; the chosen one sits a little prouder and lights its LED */
.nav-mode__option {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: var(--space-6) var(--space-3);
  border: 0;
  border-radius: var(--radius-2xl);
  /* pure white card lifted off the white page by layered shadow */
  background: var(--nix-surface);
  box-shadow: var(--lift-2);
  font-family: var(--font-sans);
  text-align: center;
  cursor: pointer;
  transition: box-shadow var(--dur-3) ease,
              transform var(--dur-3) var(--ease-out-expo);
}

.nav-mode__option:hover {
  transform: translateY(-2px);
  box-shadow: var(--lift-3);
}

.nav-mode__option-title {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 17px;
  letter-spacing: 0.02em;
  color: var(--nix-ink);
}

.nav-mode__option-desc {
  max-width: 280px;
  font-size: 14px;
  font-weight: 300;
  line-height: 1.55;
  color: var(--nix-ink-soft);
}

.nav-mode__option:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

@media (max-width: 480px) {
  .nav-mode__switch {
    grid-template-columns: 1fr;
  }
}

/* Card icon (Lucide): dim by default, wakes on hover/focus */
.nav-mode__icon {
  display: inline-flex;
  color: var(--nix-ink);
  opacity: 0.55;
  transition: opacity 300ms ease, transform 300ms var(--ease-pop);
}

.nav-mode__icon svg {
  width: 26px;
  height: 26px;
}

.nav-mode__option:hover .nav-mode__icon,
.nav-mode__option:focus-visible .nav-mode__icon {
  opacity: 1;
  transform: translateY(-1px);
}

/* Leaving the selector */
.nav-mode {
  transition: opacity 350ms var(--ease-out-expo),
              transform 350ms var(--ease-out-expo);
}

.nav-mode.is-off {
  opacity: 0;
  transform: translateY(-10px);
  pointer-events: none;
}

.btn {
  display: inline-block;
  padding: 14px 36px;
  border: 0;
  border-radius: var(--radius-pill);
  font-family: var(--font-sans);
  font-size: 14px;
  letter-spacing: 0.02em;
  text-decoration: none;
  cursor: pointer;
  transition: transform 200ms var(--ease-out-expo),
              box-shadow 200ms var(--ease-out-expo),
              background 200ms ease;
}

.btn--primary {
  background: var(--nix-ink);
  color: var(--nix-on-ink);
  box-shadow: var(--lift-control);
}

/* A GROUND AND A LIFT. The literal shadow that used to be here was the one
   hover shadow in the file that survived dark mode — which is why replacing
   it with a ground left light with a 1px translate and an unchanged contact
   shadow, reading as a slide rather than a lift. --lift-2 is `none` in dark,
   where the ground is the whole answer, and a real lift in light, where it
   always was. */
.btn--primary:hover {
  background: color-mix(in srgb, var(--nix-ink) 88%, var(--nix-paper));
  box-shadow: var(--lift-2);
  transform: translateY(-1px);
}

.btn--ghost {
  background: var(--nix-surface);
  color: var(--nix-ink);
  box-shadow: var(--lift-control);
}

/* It did nothing at all: the same --nix-surface it rests on. The secondary
   button on every landing page answered the pointer with silence. */
.btn--ghost:hover {
  background: var(--chip-hover);
  transform: translateY(-1px);
}

/* --lift-control, like its two siblings. This was a raw rgba mixed from pure
   black — the one colour literal in the component layer, and the reason this
   button was the only one of the three whose shadow survived into dark, where
   a black smear is exactly what the kill list exists to remove. */
.btn--paper {
  background: var(--nix-paper);
  color: var(--nix-ink);
  box-shadow: var(--lift-control);
}

/* A GROUND, not just a nudge. In dark this button's shadow is switched off,
   so a 1px translate was the entire response to a pointer — and the portal's
   copy of this same pill was given a step two rounds ago while the landing's
   was not. */
.btn--paper:hover {
  background: color-mix(in srgb, var(--nix-paper) 92%, var(--nix-text));
  transform: translateY(-1px);
}

/* ---- FAQ --------------------------------------------------------------
   Same hairlines as the rail. An answer is a disclosure, not a card. */
.faq {
  width: min(720px, 100%);
  margin: var(--space-8) auto 0;
  text-align: left;
  border-top: 1px solid var(--nix-rule);
}

.faq__item {
  border-bottom: 1px solid var(--nix-rule);
}

.faq__q {
  position: relative;
  list-style: none;
  cursor: pointer;
  padding: var(--space-3) var(--space-6) var(--space-3) var(--space-2);
  font-size: 18px;
  font-weight: 400;
  transition: color var(--dur-3) ease;
}

.faq__q::-webkit-details-marker {
  display: none;
}

.faq__q:hover {
  color: var(--nix-text);
}

/* a chevron, drawn from two borders — it turns over when the answer opens */
.faq__q::after {
  content: "";
  position: absolute;
  right: var(--space-3);
  top: 50%;
  width: 8px;
  height: 8px;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  opacity: 0.5;
  transform: translateY(-70%) rotate(45deg);
  transition: transform 300ms var(--ease-in-out);
}

.faq__item[open] .faq__q::after {
  transform: translateY(-30%) rotate(-135deg);
}

/* The one inset ring on the site: this row is the full width of its column
   and an outset ring would be drawn over the hairlines above and below it. */
.faq__q:focus-visible {
  outline: var(--focus-ring);
  outline-offset: -2px;
}

.faq__a {
  padding: 0 var(--space-6) var(--space-4) var(--space-2);
  font-size: 15px;
  font-weight: 300;
  line-height: 1.6;
  color: var(--nix-text-soft);
  max-width: 62ch;
}

.faq__item[open] .faq__a {
  animation: soft-rise var(--dur-4) var(--ease-out-expo);
}

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

/* Scroll reveal */
.reveal {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity var(--dur-6) var(--ease-out-expo) 140ms,
              transform var(--dur-6) var(--ease-out-expo) 140ms,
              filter var(--dur-6) var(--ease-out-expo) 140ms;
}

.reveal.is-in {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* The cipher above the copyright: a whisper, quieter than the line under it,
   monospace so the characters keep their column while they settle. */
.foot__cipher {
  margin: 0 0 6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 10px;
  letter-spacing: 0.14em;
  line-height: 1.6;
  text-align: center;
  word-break: break-all;
  color: color-mix(in srgb, var(--nix-text-soft) 45%, transparent);
}

.lang {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  letter-spacing: 0.08em;
}

.lang__opt {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  color: var(--nix-text-quiet);
  cursor: pointer;
  transition: color var(--dur-3) ease;
}

.lang__opt:hover,
.lang__opt.is-on {
  color: var(--nix-text);
}

.lang__opt:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* A GLYPH, NOT A RULE. This is a rendered `/` between EN and ES, and it was
   painted in the hairline token — which is a step, not ink, and did not exist
   on a light page. */
.lang__sep {
  color: var(--nix-text-quiet);
}

/* ---- Page reveal (under the wipe) ------------------------------------ */

.nav-mode {
  transform: none;
  transition-delay: 200ms;
}

/* ---- Reduced motion --------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .preloader {
    display: none;
  }
  .site-head__brand,
  .nav-mode {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ---- Small screens ---------------------------------------------------- */
@media (max-width: 640px) {
  .site-head {
    padding: var(--space-2) var(--space-3);
  }
  .preloader {
    padding: var(--space-6) var(--space-3);
  }
}

/* ---- Header menu: the dot opens a full-screen overlay with three cards ---- */
/* 24, THE TARGET MINIMUM, and the ring is drawn at the box's own edge so the
   drawing grows with it. It was 18 with a note beside it arguing that its
   focus outline made it "24 across" — an outline is not a target: it is not
   clickable and it is only painted on keyboard focus. The one control that
   opens the site's navigation was 44% of the area WCAG 2.5.8 asks for, on
   every page. The two offsets that place it in shell.css are arithmetic
   against half this box and moved with it. */
.site-menu__trigger {
  position: absolute;
  /* Left, matching the marketplace rail where this control already sits at
     the start of its row. The header centres the brand with flex, so the
     trigger is positioned out of that flow either way — the side is a single
     property, and the same one on every page that carries this header. */
  left: var(--space-6);
  top: 50%;
  transform: translateY(-50%);
  width: 24px;
  height: 24px;
  padding: 0;
  border: 1.5px solid var(--nix-text);
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  /* border-color, not background: the ground is transparent in both states
     and never moves, while the ring is what disappears when the trigger
     opens — so the thing that changed was the thing not being animated, and
     the border snapped out while the strokes eased in. */
  transition: border-color var(--dur-3) ease;
}

/* A POINTER GETS AN ANSWER. There was no :hover on this rule at all — the
   one control that opens the site's navigation, on every page, and the
   transition list was rewritten last round without anyone noticing there was
   no state for it to animate. */
.site-menu__trigger:hover {
  background: var(--chip-hover);
}

.site-menu__trigger:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* Open, the same button becomes the cross. The overlay used to carry its own —
   sitting under a header that now paints above it, and drifting from the real
   one besides. One button, two states. */
/* 24, with the box. The ring is a border and grew on its own; the cross is
   drawn here and did not, so an X that used to reach the ring became one that
   sits in the middle of it — and when the trigger is open the border is
   transparent, which leaves the cross alone on screen with nothing to be
   small against. */
.site-menu__trigger::before,
.site-menu__trigger::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 24px;
  height: 1.5px;
  background: var(--nix-text);
  transform: translate(-50%, -50%) scaleX(0);
  transition: transform 260ms var(--ease-out-expo);
}

.site-menu__trigger[aria-expanded="true"] {
  border-color: transparent;
}

.site-menu__trigger[aria-expanded="true"]::before {
  transform: translate(-50%, -50%) rotate(45deg) scaleX(1);
}

.site-menu__trigger[aria-expanded="true"]::after {
  transform: translate(-50%, -50%) rotate(-45deg) scaleX(1);
}

/* AND A THIRD STATE: the way back out of a view that took the screen.

   NOT THE CROSS'S TWO BARS RE-AIMED, which is what this was and what did not
   work. Those bars are 24 wide, centred, and hinged in their middle; a chevron
   needs two short arms meeting at one end, and getting there meant a width, an
   origin and a translate per arm all having to agree — one of them did not,
   and what landed on screen was a single stroke leaving the ring at the top.

   One square with two of its borders drawn, turned a quarter. It is the same
   shape every back arrow on the web is, it has no numbers in it that can
   disagree with each other, and the ring is still a border of the same weight
   in the same ink. The ring stays: a chevron alone in a corner is a mark, and
   a chevron in a circle is a button.

   :not(.is-nav) because with the panel open the ring is still the button that
   closes the panel — that is the press the reader just asked for, and the one
   after it is the one that leaves. */
/* AND NO RING BEHIND IT. Everywhere else on the site this button is a circle
   that becomes a cross; here it is one control on an otherwise empty screen
   and it means one thing, so the circle is a second mark around a mark that
   already reads. The border goes, the chevron stays, and the hit area is the
   same 24 it always was. */
body.is-quiz:not(.is-nav) .site-menu__trigger {
  border-color: transparent;
}

body.is-quiz:not(.is-nav) .site-menu__trigger::before {
  width: 7px;
  height: 7px;
  background: none;
  border-left: 1.5px solid var(--nix-text);
  border-bottom: 1.5px solid var(--nix-text);
  /* 20% rather than 50%: the arms only occupy the lower-left half of the box,
     so a box centred on the ring hangs its ink to the left of centre. */
  transform: translate(-20%, -50%) rotate(45deg);
  opacity: 0;
  animation: ring-back 240ms var(--ease-out-expo) 80ms forwards;
}

/* the second bar has nothing left to draw */
body.is-quiz:not(.is-nav) .site-menu__trigger::after {
  display: none;
}

/* IT ARRIVES, IT DOES NOT APPEAR. The view behind it fades in over a quarter
   of a second and a control that snapped on at frame one read as a glitch on
   top of it. Delayed by the same amount again so it lands after the sheet
   rather than through it. */
@keyframes ring-back {
  to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  body.is-quiz:not(.is-nav) .site-menu__trigger::before {
    animation: none;
    opacity: 1;
  }
}

/* the overlay: a full sheet holding the three menu cards */
.site-menu-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* SAFE centre. Three stacked cards do not fit a phone, and a plain `center`
     overflows in both directions in a scroll container — the top of the stack
     becomes unreachable, which is why the first card was living under the logo.
     `safe` falls back to flex-start the moment the content stops fitting. */
  justify-content: safe center;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  gap: var(--space-6);
  /* the header sits above this overlay, so leave it its own room */
  padding: calc(var(--head-h) + var(--space-3)) var(--space-6) var(--space-8);
  background: var(--nix-paper);
  opacity: 0;
  visibility: hidden;
  transition: opacity 360ms var(--ease-out-expo), visibility 360ms;
}

.site-menu-overlay[data-state="open"] {
  opacity: 1;
  visibility: visible;
}

/* the NIX logo stays put (top-centre, matching the header) while the menu is open */
/* Two cards at the index's own measurements — the same pair of pictures at
   the same size, so opening the menu reads as the index arriving rather than
   a smaller second way of saying it. */
.site-menu-overlay__grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
  width: min(820px, 92vw);
}

.menu-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-2);
  padding: var(--space-8) var(--space-3);
  border-radius: var(--radius-2xl);
  background: var(--nix-surface);
  box-shadow: var(--lift-2);
  text-decoration: none;
  color: var(--nix-ink);
  transition: transform 260ms var(--ease-out-expo),
              box-shadow 260ms var(--ease-out-expo);
}

.menu-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 2px 4px rgba(26, 26, 26, 0.06),
              0 24px 52px -16px rgba(26, 26, 26, 0.24);
}

.menu-card__name { font-size: 22px; letter-spacing: -0.01em; }

.menu-card__desc {
  font-size: 14px;
  line-height: 1.5;
  color: var(--nix-ink-soft);
  max-width: 220px;
}

/* the two things a visitor may want to change about the page itself, at the
   foot of the menu */
.site-menu-overlay__foot {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.tools__theme {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--nix-text-soft);
}

/* ---- The guide switch, in the menu ------------------------------------
   Assisted or Manual, changeable. The gate asks once and then never again,
   which is right for a question and wrong for a setting — this is where the
   setting lives. Deliberately the language switcher's own shape: two words,
   a slash, the live one brighter. gate.js builds it on every page. */
.guide {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--nix-text-soft);
}

/* The switch itself: a track the width of two knobs, and the knob on the
   side you are on. Filled when the guide is on, hollow when it is not —
   the same read as the theme toggle beside it, which is a filled circle for
   dark and a hollow one for light. */
.guide__switch {
  position: relative;
  width: 30px;
  height: 17px;
  flex: 0 0 auto;
  padding: 0;
  border: 1.5px solid var(--nix-text);
  border-radius: var(--radius-pill);
  background: transparent;
  cursor: pointer;
}

.guide__knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--nix-text);
  transition: transform 260ms var(--ease-out-expo);
}

.guide[data-mode="assisted"] .guide__switch { background: var(--nix-text); }

.guide[data-mode="assisted"] .guide__knob {
  transform: translateX(13px);
  background: var(--nix-paper);
}

.guide__switch:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.guide__opt {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  letter-spacing: 0.08em;
  color: var(--nix-text-quiet);
  cursor: pointer;
  transition: color var(--dur-3) ease;
}

/* Only one of the two is ever lit, and it is the one you are in. */
.guide__opt:hover,
.guide__opt.is-on {
  color: var(--nix-text);
}

.guide__opt:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.menu-card__soon {
  margin-top: var(--space-1);
  padding: 4px 10px;
  border: 1px solid var(--nix-rule);
  border-radius: var(--radius-pill);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--nix-ink-soft);
}

/* cards rise in when the overlay opens */
.site-menu-overlay .menu-card,
.site-menu-overlay__foot {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 500ms var(--ease-out-expo),
              transform 500ms var(--ease-out-expo);
}

.site-menu-overlay[data-state="open"] .menu-card:nth-child(1) { transition-delay: 80ms; }

.site-menu-overlay[data-state="open"] .menu-card:nth-child(2) { transition-delay: var(--dur-2); }

.site-menu-overlay[data-state="open"] .menu-card:nth-child(3) { transition-delay: 240ms; }

.site-menu-overlay[data-state="open"] .site-menu-overlay__foot { transition-delay: 320ms; }

.site-menu-overlay[data-state="open"] .menu-card,
.site-menu-overlay[data-state="open"] .site-menu-overlay__foot {
  opacity: 1;
  transform: none;
}

@media (max-width: 720px) {
  .site-menu-overlay__grid { grid-template-columns: 1fr; width: min(420px, 92vw); }
  .menu-card { padding: var(--space-4) var(--space-3); }
}

/* ---- nav-mode entrance: staggered rise as the preloader lifts --------- */
@keyframes nixNavRise {
  from { opacity: 0; transform: translateY(22px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes nixIconIn {
  from { opacity: 0; transform: scale(0.6) rotate(-14deg); }
  to   { opacity: 0.55; transform: scale(1) rotate(0deg); }
}

body.is-ready .nav-mode__sub {
  animation: nixNavRise 760ms var(--ease-out-expo) 460ms backwards;
}

body.is-ready .nav-mode__option {
  animation: nixNavRise 800ms var(--ease-out-expo) backwards;
}

body.is-ready .nav-mode__option:nth-child(1) { animation-delay: var(--dur-5); }

body.is-ready .nav-mode__option:nth-child(2) { animation-delay: 780ms; }

body.is-ready .nav-mode__option:nth-child(1) .nav-mode__icon {
  animation: nixIconIn 620ms var(--ease-pop) 760ms backwards;
}

body.is-ready .nav-mode__option:nth-child(2) .nav-mode__icon {
  animation: nixIconIn 620ms var(--ease-pop) var(--dur-6) backwards;
}

@media (prefers-reduced-motion: reduce) {
  body.is-ready .nav-mode__sub,
  body.is-ready .nav-mode__option,
  body.is-ready .nav-mode__icon { animation: none; }
}

/* THE WHOLE NAV IS ONE PILL, centred over the posts: the tags and the search
   share a single rounded container rather than sitting loose on the page.

   width: max-content is what makes it a pill and not a bar — the box is as
   wide as the words in it and `margin: auto` centres that, so adding a tag
   grows the pill instead of moving anything. max-width keeps it inside the
   panel when there are more tags than room, and the row scrolls sideways
   inside itself rather than wrapping: a nav that grows to two lines pushes
   every post down the page. */
.nix-tagbar {
  width: max-content;
  max-width: 100%;
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 22px;
  padding: 10px 20px;
  border-radius: var(--radius-pill);
  background: var(--nix-surface);
  box-shadow: var(--lift-2);
  overflow-x: auto;
  scrollbar-width: none;
}

/* Air above and below belongs to the row, not to the pill's own padding — the
   pill was sitting against the top edge of the panel. */
.nix-tagbar {
  margin-top: clamp(10px, 1.5svh, 20px);
  margin-bottom: clamp(24px, 4svh, 44px);
}

.nix-tagbar::-webkit-scrollbar { height: 0; display: none; }

/* Every item is a chip-shaped box; only the current one is filled. Giving them
   all the same padding is what keeps the row from shifting sideways when the
   filled one moves — a chip that only exists on the active item makes every
   other tag jump as you click along the row. */
/* Both spellings of every rule below. The blog renders inside #wrapwrap, where
   Odoo's bundle styles every <a> and only an id-weight selector wins; the
   standalone pages have no #wrapwrap at all. One list would lose on one of the
   two, so each says itself twice. */
.nix-tagbar__item,
#wrapwrap .nix-tagbar__item {
  flex: none;
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  color: var(--nix-text-soft);
  text-decoration: none;
  font-size: 15px;
  white-space: nowrap;
  transition: color var(--dur-2) var(--ease-out-expo), background var(--dur-2) var(--ease-out-expo);
}

.nix-tagbar__item:hover,
#wrapwrap .nix-tagbar__item:hover {
  color: var(--nix-text);
  text-decoration: none;
  background: color-mix(in oklch, var(--nix-text) 6%, transparent);
}

/* THE ITEM THAT IS A DESTINATION, not a section. It can never be `is-on` —
   sectionbar.js only tracks in-page anchors — so it carries the full text
   colour at rest instead: in a row of six quiet labels, the one that takes you
   somewhere else is the one that reads as a thing to press. */
.nix-tagbar__item--go,
#wrapwrap .nix-tagbar__item--go {
  color: var(--nix-text);
}

/* The current tag, in its own container. NOT --nix-chip: in light that token
   is #FFFFFF and so is the pill it sits in, so the chip was invisible on
   exactly the theme where a filled shape is the only thing marking the current
   tag. A share of the text colour instead — the one value guaranteed to differ
   from whatever ground it lands on, in either theme. */
.nix-tagbar__item.is-on,
#wrapwrap .nix-tagbar__item.is-on {
  color: var(--nix-text);
  background: color-mix(in oklch, var(--nix-text) 10%, transparent);
}

/* ── The same bar, on the two standalone pages ───────────────────────────────
   AB01 and /services have no tags; what they have is sections, so the items
   are in-page anchors and the current one is worked out by sectionbar.js from
   where the reader has scrolled to.

   Sticky, which the blog's is not: a tag bar is a filter you use once and a
   section bar is a table of contents you use all the way down. Inside .page,
   because that is the box that scrolls on these pages — the document does
   not. */
/* ONE NAVIGATION AT A TIME. A page whose sections are also rows in the rail
   has two of them the moment the rail is out — the same seven words in two
   shapes, eight inches apart, and the pill is the one that is over the page
   rather than beside it. Out, the rail carries them; shut or a strip, the
   pill is the only table of contents there is and it comes back.

   :has(.nav-rail__drill) is what decides which pages this applies to, and it
   asks the markup rather than a list of paths: the page that gives its own
   bar to the rail is the page that loses that bar, and it says so by having
   a second level. A page with a bar and no second level — the blog's tags —
   keeps it, because hiding it there would take away that page's only way
   around itself.

   THREE BARS BY NAME, because they are three different things doing one job:
   .nix-sectionbar is the table of contents on /agent-base and /services,
   .mk-shelf is the marketplace's five shelves, and .nix-blogbar is the blog's
   tags. The rule is the same for all of them and the shapes are not shared,
   so each is named rather than one being bent into another's class. */
body.is-nav:has(.nav-rail__drill) .nix-sectionbar,
body.is-nav:has(.nav-rail__drill) .mk-shelf,
body.is-nav:has(.nav-rail__drill) .nix-blogbar {
  display: none;
}

.nix-sectionbar {
  position: sticky;
  top: 8px;
  z-index: 30;
  /* The pill is a surface floating over the page's own ground, and on these
     two pages that ground is what shows through the gap around it. */
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

/* ── THE PILL STARTS AFTER THE RING ──────────────────────────────────────
   On a phone the rail covers the page rather than standing beside it, so the
   ring is the only way into the menu and it sits at the panel's top-left —
   which is where this bar's first word was. "What it is" was drawn with a
   circle through it, and worse, the two controls shared those pixels: a tap
   there was a coin toss between opening the menu and jumping to a section.

   The bar gives up the room instead of the ring moving, because the ring is
   in that corner on every page this site has and these bars are on four of
   them. 56px is the ring's own 25 + 20 with a thumb's clearance after it; the
   right side keeps the measure it already had. The items scroll, so what this
   costs is a few pixels of a row that was already scrollable.

   ALL FOUR BARS, because the ring does not care which page it is on. The two
   landings' table of contents and the blog's tags are the bar itself and take
   the room directly; the marketplace's is a ROW around its pill and takes it
   below. Fixing one of them and leaving the others is how "Pricing" got
   reported once and "Releases" would have been reported next. */
@media (max-width: 720px) {
  .nix-sectionbar,
  .nix-blogbar {
    margin-inline: 56px 24px;
    /* AND THE MEASURE HAS TO PAY FOR THEM. `max-width: 100%` up in
       .nix-tagbar resolves against the CONTAINING BLOCK, and margins are laid
       outside that — they are not deducted. So the pill came out its full
       100% and then took 80px of margin on top: eighty pixels wider than the
       room it had, with its right cap off the side of the screen and Pricing
       cut through the middle of the word. It read as a deliberate
       edge-to-edge scroller, which is the only reason it survived a commit.

       Subtracting the pair here rather than dropping the margins, because
       the left one is what keeps this bar off the ring. */
    max-width: calc(100% - 56px - 24px);
  }

  /* THE MARKETPLACE'S TAKES THE SAME PAIR, IN ITS OWN SHEET. It is the third
     bar and the rule is the same one — but pages/store.css is linked after
     this file and gives .mk-shelf its centred margin at the same specificity,
     so written here it would be overwritten at exactly the width it matters.
     It lives beside that rule instead, where the account's two panel-wide
     resets also have to be answered. */
}

/* The description sleeps under the label and wakes on hover — the card
   says one thing at rest and explains itself when asked. Focus counts as
   asking; a keyboard is not a lesser pointer. */
.menu-card--art .menu-card__desc {
  position: relative;
  z-index: 1;
  font-size: 15px;
  color: var(--nix-text-soft);
  max-width: 22em;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 250ms ease, transform 250ms ease;
}

.menu-card--art:hover .menu-card__desc,
.menu-card--art:focus-visible .menu-card__desc {
  opacity: 1;
  transform: none;
}

/* ---- the orbit inside the Services card ----------------------------- *
 * The /services hero, shrunk into the card that leads there: the Odoo
 * catalogue on two rings, cropped to the card's own bottom edge so only
 * the crown of the arcs shows.
 *
 * Its own class names rather than .orbit's. That component is sized for
 * a full-bleed hero — a 64px satellite on a ring as wide as the viewport
 * — and reusing the names here would mean overriding almost every
 * declaration in it, which is not reuse, it is a second component
 * wearing the first one's clothes.
 * -------------------------------------------------------------------- */
/* The index card is not the overlay card. It has no icon and no
   description — the artwork says what the words used to, and the label
   sits at the foot under it. Scoped to the grid so the overlay, which
   still has all three parts, is untouched.

   justify-content works because the artwork is absolutely positioned:
   the label is the only child left in flow, so "push everything down"
   pushes exactly it. */
.menu-card--art {
  justify-content: flex-end;
  /* tall enough for the agents panel and its label: the panel is three rows
     of 16:10 tiles, and 300px left it sitting on the type */
  min-height: 362px;
  /* less room under the label than around it, so the name sits low in the
     card and the artwork above it gets the air */
  padding: var(--space-3) var(--space-3) 18px;
  /* the name and the description it wakes are one block, not two rows of a
     list: 16px between them read as a gap the card had forgotten to fill */
  gap: 4px;
  /* In dark, no shadow: the surface is already a step lighter than the page,
     and a drop shadow under it on that ground only muddies the corner. In
     light the surface is white on a near-white page and has no edge at all —
     the card disappeared and left artwork and a label loose on the page. The
     shadow is what a card is made of there. */
  box-shadow: var(--lift-2);
  /* How much of the foot the label takes: its own line plus the card's
     bottom padding. The artwork centres itself in what is left, so this
     is the one number that decides where both pieces sit. */
  --art-foot: 56px;
}

html[data-theme="dark"] .menu-card--art { box-shadow: none; }

/* No lift on these two. The overlay's cards still take it — there a card is
   a button in a list and the rise is the affordance. Here the card is a
   framed picture with eight cursors moving inside it, and shifting the whole
   frame under the pointer reads as the picture flinching.

   The shadow is not reset here any more: in light it is the card's only
   edge, and clearing it on hover made the card vanish under the pointer. */
.menu-card--art:hover,
.menu-card--art:focus-visible {
  transform: none;
}

.menu-card--art .menu-card__name {
  font-size: 24px;
  /* above the artwork, which reaches up behind it */
  position: relative;
  z-index: 1;
}

/* Both cards that carry artwork. It runs past them on every side; the
   card's own rounded corner is the crop. */
.menu-card--art {
  position: relative;
  overflow: hidden;
}

/* Centred in the band above the label, not hung off the card's foot.
   inset + a fixed size + margin:auto is what centres an absolutely
   positioned box in both axes at once; the ring is wider than the card and
   the same rule still splits the overflow evenly between the two sides. */
.card-orbit {
  --size: 320px;
  position: absolute;
  inset: 0 0 var(--art-foot) 0;
  margin: auto;
  width: var(--size);
  /* a slice off the top of the circle, not its whole half — the card has
     room for the crown of the arc and nothing more. Everything the markup
     places has to sit inside this band: at --size 320 that is roughly
     -150deg to -30deg on the outer ring, and anything outside it is
     positioned into the crop. */
  /* The chip riding the crown is centred ON the ring, so half of it sits
     above the arc's highest point — which is the top of the stage. Without
     headroom this box cropped it in half, and the app at the top of the
     card came out sliced. The stage is pushed down by the same amount, so
     the arc lands where it did and only the overhang is new. */
  --crown: 21px;
  height: calc(110px + var(--crown));
  overflow: hidden;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 40%, transparent 98%);
  mask-image: linear-gradient(to bottom, #000 40%, transparent 98%);
}

/* the full square the rings are drawn in, top-aligned inside that slice */
.card-orbit__stage {
  position: absolute;
  top: var(--crown);
  left: 0;
  width: var(--size);
  height: var(--size);
}

.card-orbit__ring {
  position: absolute;
  border: 1px solid var(--nix-rule);
  border-radius: 50%;
}

.card-orbit__ring--outer { inset: 0; }

.card-orbit__ring--inner { inset: 19%; }

/* The centre of the rings — what the apps are going around. It sits on the
   stage's midpoint, which is the same point every satellite's --a angle is
   measured from, so it lands on the rings' true centre rather than the box's.
   The two are not the same: the stage is a full square and the box is a crop
   of its top.

   Width in px against a --size that varies, deliberately. The inner ring's
   radius is about 31% of --size — roughly 80px at the size this card uses —
   and the wordmark has to stay clear of it. A percentage would grow into the
   ring on a wide card. */
.card-orbit__hub {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 58px;
  opacity: .85;
}

.card-orbit__hub img { width: 100%; height: auto; display: block; }

/* satellites ride the rings; the second rotate keeps each icon upright */
/* The two rings turn against each other, on the same keyframes and the same
   durations as /services — that pair is the mechanism, and a card that
   turned at its own speed would read as a different one. Each satellite's
   second rotate is what keeps its icon upright while the ring carries it. */
.card-orbit__sat {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--nix-chip);
  box-shadow: 0 1px 1px rgba(26, 26, 26, 0.06),
              0 3px 7px -2px rgba(26, 26, 26, 0.12);
  will-change: transform;
  /* the animation's own first frame, written out. The animation overrides
     it while it runs; without it, a reader with reduced motion — where the
     animation is switched off — would get thirteen chips stacked on the
     centre point instead of a ring held still.

     --a is each satellite's angle and it comes from the markup. The 0deg
     fallback is what keeps this a ring if a chip ever ships without one:
     without it the whole declaration is invalid and every satellite lands on
     the centre. */
  transform: rotate(var(--a, 0deg)) translateX(var(--r)) rotate(calc(-1 * var(--a, 0deg)));
}

.card-orbit__sat--outer {
  --r: calc(var(--size) / 2);
  width: 38px;
  height: 38px;
  margin: -19px 0 0 -19px;
  border-radius: 11px;
  animation: orbit-cw 90s linear infinite;
}

.card-orbit__sat--inner {
  --r: calc(var(--size) * 0.31);
  width: 32px;
  height: 32px;
  margin: -16px 0 0 -16px;
  border-radius: 10px;
  animation: orbit-ccw 60s linear infinite;
}

/* The source PNGs are 100x100. At the old 30px chip the icon drew at 18.6
   CSS pixels — a five-to-one downscale, which is where Chrome's filter gives up and
   the edges go crunchy. a 25px glyph on a 38px chip is four-to-one on a 1x screen and two-to-one
   on a 2x one, and the icons are large enough to read besides. */
.card-orbit__sat img { width: 66%; height: 66%; display: block; }

@keyframes orbit-cw {
  from {
    transform: rotate(var(--a, 0deg)) translateX(var(--r))
               rotate(calc(-1 * var(--a, 0deg)));
  }
  to {
    transform: rotate(calc(var(--a, 0deg) + 360deg)) translateX(var(--r))
               rotate(calc(-1 * var(--a, 0deg) - 360deg));
  }
}

@keyframes orbit-ccw {
  from {
    transform: rotate(var(--a, 0deg)) translateX(var(--r))
               rotate(calc(-1 * var(--a, 0deg)));
  }
  to {
    transform: rotate(calc(var(--a, 0deg) - 360deg)) translateX(var(--r))
               rotate(calc(-1 * var(--a, 0deg) + 360deg));
  }
}

/* the brand dot out for a walk on the outer ring, as on /services */
.card-orbit__spark {
  position: absolute;
  top: 50%;
  left: 50%;
  --r: calc(var(--size) / 2);
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  border-radius: 50%;
  background: var(--focus-ink);
  transform: rotate(var(--a, 0deg)) translateX(var(--r)) rotate(calc(-1 * var(--a, 0deg)));
  animation: orbit-cw 36s linear infinite;
}

/* ---- the orb inside the Products card ------------------------------- *
 * The guide itself. assist.js offers its scene as NIX.dropScene and the
 * page lights one here, the same way AB01's machine already does — the
 * drop leans toward the pointer and answers to being dragged across
 * because it IS the scene, not a second drawing of it.
 *
 * Square, because the scene sizes itself off this box and a drop in a
 * rectangle is an ellipse.
 *
 * Everything below the canvas is the stand-in: what shows while three.js
 * is on its way, and what stays if it never comes — no WebGL, or a reader
 * who asked for no motion and is never sent the download. startOrb marks
 * this box is-live when the canvas takes over, and it steps aside then.
 * -------------------------------------------------------------------- */
/* ---- the agents panel inside the Agent Base card --------------------- *
 * AB01's Agents shot at a third of the size: the head, the New agent tile
 * and the same eight screenshots, and nothing else. The tiles carry no name
 * bars — at this scale a name bar is a grey smudge, and what survives
 * shrinking is the arrangement, not the text.
 * -------------------------------------------------------------------- */
/* A panel sitting inside the card, not a full bleed across it: it keeps its
   own paper and its own corner, and nothing else — the darker ground is
   enough to lift it off the card, and a drop shadow on top of that read as
   a sticker. Centred by transform rather than by inset + margin:auto, which the
   orbit and the drop both use — those two have a fixed size, this one is as
   tall as its rows make it, and a box with both insets pinned stretches
   instead of hugging its content. */
.card-shot {
  position: absolute;
  left: 50%;
  /* half the label's band, and 14px more so the panel is not resting on the
     type */
  top: calc(50% - var(--art-foot) / 2 - 14px);
  transform: translate(-50%, -50%);
  width: min(88%, 340px);
  padding: 9px 10px 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--nix-paper);
  border-radius: var(--radius-chip);
  /* The panel's ground IS the page's ground, so on a light page it had no
     edge at all and the miniature read as tiles scattered on the card. The
     shadow is what gives it one — the same pair every surface on this site
     is lifted by, and in dark it costs nothing visible. */
  box-shadow: var(--lift-1);
}

/* ── The marketplace card's miniature ──────────────────────────────────────
   A shelf of agents for sale. Deliberately the SAME panel the Agents shot
   uses — same paper, same radius, same lift — because both cards lead into
   the same product and a third visual language on this row would read as a
   third product.

   What makes it a STORE rather than a second Agents grid is the row shape:
   a name bar and a price bar under each tile, which is what a listing has
   and a running agent does not. No screenshots on purpose — this is the
   simple version, and a picture per tile would need eight assets that do not
   exist yet for agents nobody has published.

   It rides .card-shot for position and chrome; only the tile interior is
   its own. */
.card-shelf__cell {
  position: relative;
  aspect-ratio: 16 / 13;
  border-radius: 5px;
  overflow: hidden;
  background: var(--nix-surface);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 3px;
  padding: 5px;
}

/* The two bars: a wide one for the name, a short one for the price. Their
   widths vary per tile so the shelf reads as different agents rather than a
   repeating pattern — set inline with --w on each cell. */
.card-shelf__line {
  height: 3px;
  border-radius: 2px;
  background: var(--nix-text);
  opacity: .22;
  width: var(--w, 70%);
}

.card-shelf__line--price {
  width: var(--p, 28%);
  opacity: .38;
}

/* ── The marketplace card's miniature, second version ──────────────────────
   One featured listing across the top, a heading, and the companies selling
   under it in a row. It replaced a nine-tile shelf, which at this size was
   the Agents grid beside it in different clothes — same nine boxes, same
   rhythm, and a reader saw one picture printed twice.

   The blocks are washes of the text colour rather than a ground token: on a
   light page --nix-surface and --nix-paper are both white, so a tile drawn
   in surface on the paper frame has no edge at all. A share of the text
   colour inverts with the theme on its own. */
.card-store {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
  height: 100%;
}

.card-store__hero {
  /* takes whatever the label and the row leave, so the frame is always full
     and the proportion holds at any card width */
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 5px;
  padding: 10px;
  border-radius: 10px;
  background: color-mix(in oklch, var(--nix-text) 7%, transparent);
}

.card-store__bar {
  height: 4px;
  border-radius: 2px;
  background: var(--nix-text);
  opacity: .22;
  width: var(--w, 60%);
}

.card-store__bar--price {
  width: var(--p, 26%);
  opacity: .38;
}

.card-store__label {
  font-size: var(--text-2xs);
  color: var(--nix-text-soft);
}

.card-store__row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
}

.card-store__tile {
  aspect-ratio: 1;
  border-radius: var(--radius-sm);
  background: color-mix(in oklch, var(--nix-text) 7%, transparent);
}

.card-shot__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.card-shot__title {
  font-size: var(--text-2xs);
  color: var(--nix-text);
}

.card-shot__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 5px;
}

.card-shot__cell,
.card-shot__new {
  position: relative;
  aspect-ratio: 16 / 10;
  border-radius: 5px;
  overflow: hidden;
  background: var(--nix-surface);
}

.card-shot__cell img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The named cursors AB01's own shot draws, at this scale: an arrow with the
   agent's name riding behind it, so the grid reads as eight machines each
   being driven by someone. Colour is the one place this card uses any, and
   it is inside the mock only. Same eight colours the full-size panel gives
   the same eight agents — a different palette here would read as a
   different product. They walk the same four paths too, at the same
   durations and the same negative delays, so no two are ever in step. */
.card-shot__ptr {
  position: absolute;
  left: 8%;
  top: 14%;
  width: 9px;
  height: 9px;
  z-index: 2;
  --c: #4a86f7;
}

.card-shot__ptr svg {
  width: 9px;
  height: 9px;
  display: block;
  fill: var(--c, var(--nix-text));
  stroke: #fff;
  stroke-width: 1.8;
  stroke-linejoin: round;
}

.card-shot__ptr b {
  position: absolute;
  left: 7px;
  top: 7px;
  background: var(--c, var(--nix-text));
  color: #fff;
  font-weight: 400;
  font-size: 6px;
  line-height: 1;
  letter-spacing: 0.01em;
  padding: 2px 4px 3px;
  border-radius: 6px;
  white-space: nowrap;
}

/* the ring that fires where a pointer stops */
.card-shot__ptr i {
  position: absolute;
  left: -4px;
  top: -4px;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  border: 1.2px solid var(--c, var(--nix-text));
  opacity: 0;
}

.card-shot__cell:nth-of-type(1) .card-shot__ptr { --c: #4a86f7; }

.card-shot__cell:nth-of-type(2) .card-shot__ptr { --c: #4caf6d; }

.card-shot__cell:nth-of-type(3) .card-shot__ptr { --c: #f2c14e; }

.card-shot__cell:nth-of-type(4) .card-shot__ptr { --c: #8b5cf6; }

.card-shot__cell:nth-of-type(5) .card-shot__ptr { --c: #e8674c; }

.card-shot__cell:nth-of-type(6) .card-shot__ptr { --c: #21b0c4; }

.card-shot__cell:nth-of-type(7) .card-shot__ptr { --c: #d2588f; }

.card-shot__cell:nth-of-type(8) .card-shot__ptr { --c: #6b7fd7; }

/* a yellow pill needs dark text to stay legible */
.card-shot__cell:nth-of-type(3) .card-shot__ptr b { color: #1a1a1a; }

/* Four paths shared out among eight machines, each with its own duration
   and a negative delay so the card is never caught with all eight starting
   together. The distances are AB01's own, scaled by the ratio between the
   two pointers and their tiles: the same percentages travel a shorter way
   here, because a translate in % is a share of the arrow, not of the tile
   it walks across. */
.card-shot__ptr,
.card-shot__ptr i {
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

.card-shot__ptr i { animation-name: card-shot-click; }

.card-shot__cell:nth-of-type(1) .card-shot__ptr,
.card-shot__cell:nth-of-type(1) .card-shot__ptr i { animation-duration: 11s; animation-delay: -2s; }

.card-shot__cell:nth-of-type(1) .card-shot__ptr { animation-name: card-shot-ptr-a; }

.card-shot__cell:nth-of-type(2) .card-shot__ptr,
.card-shot__cell:nth-of-type(2) .card-shot__ptr i { animation-duration: 13s; animation-delay: -5s; }

.card-shot__cell:nth-of-type(2) .card-shot__ptr { animation-name: card-shot-ptr-b; }

.card-shot__cell:nth-of-type(3) .card-shot__ptr,
.card-shot__cell:nth-of-type(3) .card-shot__ptr i { animation-duration: 15s; animation-delay: -9s; }

.card-shot__cell:nth-of-type(3) .card-shot__ptr { animation-name: card-shot-ptr-c; }

.card-shot__cell:nth-of-type(4) .card-shot__ptr,
.card-shot__cell:nth-of-type(4) .card-shot__ptr i { animation-duration: 12s; animation-delay: -3s; }

.card-shot__cell:nth-of-type(4) .card-shot__ptr { animation-name: card-shot-ptr-d; }

.card-shot__cell:nth-of-type(5) .card-shot__ptr,
.card-shot__cell:nth-of-type(5) .card-shot__ptr i { animation-duration: 14s; animation-delay: -7s; }

.card-shot__cell:nth-of-type(5) .card-shot__ptr { animation-name: card-shot-ptr-a; }

.card-shot__cell:nth-of-type(6) .card-shot__ptr,
.card-shot__cell:nth-of-type(6) .card-shot__ptr i { animation-duration: 16s; animation-delay: -4s; }

.card-shot__cell:nth-of-type(6) .card-shot__ptr { animation-name: card-shot-ptr-b; }

.card-shot__cell:nth-of-type(7) .card-shot__ptr,
.card-shot__cell:nth-of-type(7) .card-shot__ptr i { animation-duration: 12.5s; animation-delay: -8s; }

.card-shot__cell:nth-of-type(7) .card-shot__ptr { animation-name: card-shot-ptr-c; }

.card-shot__cell:nth-of-type(8) .card-shot__ptr,
.card-shot__cell:nth-of-type(8) .card-shot__ptr i { animation-duration: 13.5s; animation-delay: -6s; }

.card-shot__cell:nth-of-type(8) .card-shot__ptr { animation-name: card-shot-ptr-d; }

/* a: reads a page top-down, then jumps back up */
@keyframes card-shot-ptr-a {
  0%       { transform: translate(0, 0);        opacity: 0; }
  6%       { opacity: 0.95; }
  24%      { transform: translate(285%, 190%);  opacity: 0.95; }
  34%, 40% { transform: translate(285%, 190%); }
  58%      { transform: translate(700%, 80%);   opacity: 0.95; }
  66%, 72% { transform: translate(700%, 80%); }
  88%      { transform: translate(160%, 445%);  opacity: 0.8; }
  100%     { transform: translate(0, 0);        opacity: 0; }
}

/* b: works a wide row left to right, low on the tile */
@keyframes card-shot-ptr-b {
  0%       { transform: translate(580%, 405%);  opacity: 0; }
  8%       { opacity: 0.95; }
  22%      { transform: translate(80%, 340%);   opacity: 0.95; }
  30%, 37% { transform: translate(80%, 340%); }
  55%      { transform: translate(405%, 55%);   opacity: 0.95; }
  63%, 70% { transform: translate(405%, 55%); }
  86%      { transform: translate(810%, 285%);  opacity: 0.8; }
  100%     { transform: translate(580%, 405%);  opacity: 0; }
}

/* c: short, hesitant hops around one corner */
@keyframes card-shot-ptr-c {
  0%       { transform: translate(120%, 55%);   opacity: 0; }
  7%       { opacity: 0.95; }
  19%      { transform: translate(460%, 120%);  opacity: 0.95; }
  28%, 34% { transform: translate(460%, 120%); }
  46%      { transform: translate(340%, 350%);  opacity: 0.95; }
  54%, 61% { transform: translate(340%, 350%); }
  74%      { transform: translate(635%, 430%);  opacity: 0.95; }
  82%, 89% { transform: translate(635%, 430%); }
  100%     { transform: translate(120%, 55%);   opacity: 0; }
}

/* d: one long diagonal sweep, then straight back */
@keyframes card-shot-ptr-d {
  0%       { transform: translate(755%, 40%);   opacity: 0; }
  9%       { opacity: 0.95; }
  31%      { transform: translate(110%, 445%);  opacity: 0.95; }
  41%, 49% { transform: translate(110%, 445%); }
  68%      { transform: translate(700%, 340%);  opacity: 0.95; }
  77%, 84% { transform: translate(700%, 340%); }
  100%     { transform: translate(755%, 40%);   opacity: 0; }
}

@keyframes card-shot-click {
  0%, 33%   { opacity: 0;   transform: scale(0.4); }
  36%       { opacity: 0.7; transform: scale(0.4); }
  44%       { opacity: 0;   transform: scale(1.1); }
  65%       { opacity: 0;   transform: scale(0.4); }
  68%       { opacity: 0.7; transform: scale(0.4); }
  76%, 100% { opacity: 0;   transform: scale(1.1); }
}

/* A reader who asked for no motion keeps eight parked cursors, not eight
   invisible ones: the paths start and end at opacity 0. */
@media (prefers-reduced-motion: reduce) {
  .card-shot__ptr,
  .card-shot__ptr i { animation: none; }
  .card-shot__ptr { opacity: 0.95; }
}

.card-shot__new {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--nix-text-soft);
  font-size: 9px;
}

.card-orb {
  --orb: 172px;
  position: relative;
  /* a span, and it used to be absolutely positioned — which blockified it for
     free. In flow it is inline until told otherwise, and an inline box ignores
     both of these, which is a 0x0 drop */
  display: block;
  width: var(--orb);
  height: var(--orb);
}

/* The stand-in's glow, on a pseudo-element and not on the box itself: the
   blur belongs to the gradients only, and applied to .card-orb it would
   have smeared the canvas sitting inside it too. */
.card-orb::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* the last layer is the haze the ball sits in — it reaches past the
     others and fades to nothing, so there is no circle edge anywhere */
  background:
    radial-gradient(38% 38% at 63% 66%, rgba(242, 158, 205, 0.95), transparent 72%),
    radial-gradient(40% 40% at 34% 70%, rgba(146, 219, 243, 0.88), transparent 74%),
    radial-gradient(52% 52% at 47% 37%, rgba(134, 134, 242, 0.95), transparent 74%),
    radial-gradient(closest-side, rgba(216, 219, 250, 0.62), rgba(216, 219, 250, 0) 84%);
  filter: blur(13px);
  transition: opacity 1400ms var(--ease-out-expo);
}

/* Not inert: the scene reads pointerenter, pointerleave and pointermove off
   this box, and pointer-events:none would hand it a drop nobody can touch.
   The card is a link and this sits inside it, so a click still opens
   /products — the scene never calls preventDefault. */
.card-orb__canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 700ms var(--ease-out-expo);
}

.card-orb__canvas.is-live { opacity: 1; }

/* Once the canvas paints, the stand-in behind it is a coloured halo in the
   ring the sphere does not fill, and two bars where the real eyes are not.

   Faded, not switched off. display:none is instantaneous, and it fired the
   moment the canvas went live — while the canvas was still fading up. One
   thing vanishing under another thing arriving is the flash that read as the
   sphere appearing all at once. */
.card-orb.is-live::before { opacity: 0; }

/* Stopped, not hidden. The rings are the picture; a reader who has asked
   for no motion should still see the picture, just held still. Written
   here rather than in one of the blocks above because those two are the
   /services and AB01 stories and this is neither. */
@media (prefers-reduced-motion: reduce) {
  .card-orbit__sat,
  .card-orbit__spark { animation: none; }
}

/* ── The foot ────────────────────────────────────────────────────────────── */
.index-foot {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
  /* Full-bleed ground under a page-width grid: the negative inline margin
     takes it out to the window's edges, and the padding puts its contents
     back on the column everything else sits on. Without it the foot is a
     floating slab with the page ground showing either side of it. */
  /* No gap above it any more: the band ends and the foot begins, which is
     what makes the two read as one edge rather than two slabs with page
     showing between them. */
  margin: 0 calc(50% - 50vw);
  padding: clamp(40px, 6svh, 72px) max(calc(50vw - 590px), 4vw) var(--space-4);
  /* THE PAGE'S OWN GROUND, the same one the hero stands on. It used to be
     mixed 8% toward black to sit a step below the page; now the two sections
     that bracket the content — the drop at the top, the foot at the bottom —
     are one surface, and the band in the middle is the only thing lifted off
     it. Still a token rather than a hex, for the reason the mix was: a
     literal would be right in one theme and wrong in the other. */
  background: var(--nix-paper);
}

/* Sized to the line it now sits on rather than to the block it used to head.
   28px was a logo standing on its own above four columns; beside 13px type it
   is a badge shouting over the sentence it belongs to. */
.index-foot__mark { height: 18px; width: auto; flex: none; }

/* THE BOTTOM BAR. A rule over it, the signature on one side and the accounts
   on the other — so the base64 has something to belong to instead of floating
   under the page, and four glyphs stop setting the brand column's width. */
.index-foot__bar {
  /* Three cells, and the outer two are the SAME width so the middle one is
     centred against the bar rather than against whatever is left over.
     space-between would put the cipher wherever the copyright and the icons
     happened to leave it, which is centred by luck and off-centre the moment
     either side changes length. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--nix-rule);
}

.index-foot__sign {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Pushed to the far end of its own cell, so the row reads left / centre /
   right rather than three things clustered in the middle. */
.index-foot__social { display: flex; gap: 14px; justify-self: end; }

.index-foot__social a {
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  color: var(--nix-text-soft);
}

.index-foot__social a:hover { color: var(--nix-text); }

.index-foot__social svg { width: 100%; height: 100%; }

/* The cipher sits under the legal line, quieter than it — monospace so the
   characters hold their column while they settle. */
.index-foot__cipher {
  /* No top margin now that it sits in a row rather than under one. */
  margin: 0;
  text-align: center;
  /* It may wrap on a narrow bar, but it must never push the icons off the
     end of it — a decoder line is not worth a broken row. */
  min-width: 0;
  overflow-wrap: anywhere;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: var(--text-2xs);
  letter-spacing: .04em;
  color: var(--nix-text-quiet);
  word-break: break-all;
}

.index-foot__cols {
  display: grid;
  /* auto-fit so four columns become two on a tablet and one on a phone
     without a breakpoint apiece. Back to 1fr from a 240px cap: the cap existed
     to keep the rule under each heading short, and there is no rule now — what
     it cost was a fourth track, so "Get in touch" wrapped under "Products"
     while two thirds of the row sat empty. */
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-3);
}

.index-foot__col {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* THE HEADING IS A HEADING, not a label. It was 13px uppercase in the soft
   text colour and the links under it were full-strength — so the four words
   that say what a list IS were the quietest thing in the foot, and the reader
   met twelve links of equal weight with faint captions over them.

   Turned around: the heading takes the text colour, the links step back to
   soft. Colour alone does the separating; a rule under each heading drew four
   lines across the foot and the eye read the lines before the words. */
.index-foot__head {
  font-size: 14px;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--nix-text);
  margin-bottom: 8px;
}

.index-foot__col a {
  color: var(--nix-text-soft);
  text-decoration: none;
  font-size: 14px;
  transition: color var(--dur-2) var(--ease-out-expo);
}

.index-foot__col a:hover { color: var(--nix-text); }

.index-foot__legal {
  color: var(--nix-text-soft);
  font-size: 13px;
}

@media (max-width: 720px) {
  /* The brand sits above the columns on a narrow screen either way, so the
     only thing to fix is the inset — max() holds it off the window's edge
     once the 590px half-measure stops applying. */
  .index-foot { padding-left: 5vw; padding-right: 5vw; }
  /* The bar stacks: three cells across 360px gives each about a hundred
     pixels, and the cipher alone is longer than that. Centred all through,
     since there is no longer a left and a right to range against. */
  .index-foot__bar { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .index-foot__social { justify-self: center; }
}

/* NO SHADOWS ON A DARK GROUND. A shadow is light being blocked, and on #101010
   there is none to block: what the browser draws instead is a black smear that
   reads as dirt around the card. The step between --nix-panel and
   --nix-surface is the whole edge in dark, and it is enough — the light theme
   keeps its shadows, where the two grounds are a per cent apart and the lift
   IS the edge.

   Listed rather than swept with a `*`: box-shadow is also how this stylesheet
   draws a few hairlines (a zero-blur shadow is a border wearing a moustache),
   and turning those off would take real lines with it.

   Only the literals are listed. Anything in this file that spends --lift-* is
   already answered: those tokens are `none` on this ground. What is left is
   the three buttons and the chips on the orbit, which draw their own. */
html[data-theme="dark"] .nix-tagbar,
html[data-theme="dark"] .menu-card,
html[data-theme="dark"] .btn--primary:hover,
html[data-theme="dark"] .btn--ghost,
html[data-theme="dark"] .btn--paper,
html[data-theme="dark"] .card-orbit__sat { box-shadow: none; }

/* ── The phone ────────────────────────────────────────────────────────────
   The card artwork at a phone's scale: a nearly square card, a shorter panel
   inside it, and two rows of tiles instead of three. */
@media (max-width: 720px) {
  /* No hover on a phone: the description is simply there. */
  .menu-card--art .menu-card__desc { opacity: 1; transform: none; font-size: 14px; }
  /* Nearly square, where the grid is one column and a card as wide as the
     screen at 236px tall was a letterbox. The height comes off the width now,
     so it holds whatever the screen is. Enough frame to read as a card and no
     more: the desktop keeps about a card's-width-in-eight of air around the
     panel, and this is that at a phone's scale. */
  .menu-card--art {
    min-height: 0;
    aspect-ratio: 5 / 4;
    --art-foot: 46px;
    padding: 16px 16px 14px;
  }
  .menu-card--art .menu-card__name { font-size: 20px; }
  /* The same proportion the desktop card holds: the panel takes most of the
     card and leaves a little air, rather than the stamp-in-a-frame it was at
     300px or the full bleed it became. */
  .card-shot {
    width: 92%;
    top: calc(50% - var(--art-foot) / 2 - 10px);
    padding: 8px 9px 9px;
    gap: 5px;
  }
  .card-shot__grid { gap: 4px; }
  .card-shot__title { font-size: 10px; }
  /* A phone card is half the height and nine tiles do not fit in it. Two rows
     and the New tile keep the panel legible as a panel; six tiles still read
     as a shelf. */
  .card-shot__cell:nth-of-type(n + 6) { display: none; }
  .card-shelf__cell:nth-of-type(n + 7) { display: none; }
  .card-orbit { --size: 260px; height: calc(84px + var(--crown)); }
}

/* ── The story pages ──────────────────────────────────────────────────────
   /products and /services are the same kind of document: a fixed bar, a hero
   that fills the first screen, then blocks a reader scrolls through. What
   follows is that shape, and it is here rather than in either page's sheet
   because both wear it — /products was about to open with no hero at all
   when the sheets were first split. */
.page--flow {
  display: block;
  /* named, because the hero's orbit spends the same number in reverse: it
     breaks out of the reading column onto the panel's two edges and has to
     add back exactly what the panel holds the column in by. Two literals
     drift apart at the phone breakpoint below; one token cannot. */
  --flow-gutter: var(--space-6);
  padding: 0 var(--flow-gutter) var(--space-8);
}

/* AND NONE OF IT UNDER THE FOOT. That bottom padding is written for a story
   that ends in a sentence — it is the air under the last line. When the last
   thing in the scroller is the foot, it is air under a slab that carries its
   own padding, and in the rail layout the foot is transparent, so it read as
   the foot stopping short of the panel's bottom edge with a band of panel
   below it. The rounded corner the foot is given there landed in the middle
   of nothing.

   This was fixed once, for the portal leg only — `#wrapwrap.nav-shell
   main.page` — which is the blog. /services and /products are standalone
   documents, they never match #wrapwrap, and they are exactly the two pages
   the reader saw it on. Asking whether the foot IS the end of the page is the
   condition the old rule was approximating with a URL. */
main.page:has(> .index-foot:last-child) {
  padding-bottom: 0;
}

/* AND IT DOES NOT ARRIVE BEFORE THE PAGE DOES. /services ships its whole
   story inside `<section id="landing" hidden>` — the guide decides whether to
   offer a chooser first, and nix.js takes 350ms to answer. For those 350ms
   the only thing in the scroller with any height is the foot, so the first
   thing painted on the page was four columns of links and a copyright,
   which then shot to the bottom when the story arrived. Reading it as a
   flash is generous; it is a whole layout, drawn once, of a page that has
   nothing in it yet.

   Keyed to the attribute rather than to a class the script adds, so there is
   no third state to keep in step: the foot is invisible for exactly as long
   as the page above it is, and neither JavaScript nor a body class is
   involved in the change. visibility, not display — the foot keeps its box,
   so nothing reflows when it appears. */
main.page:has(> #landing[hidden]) > .index-foot {
  visibility: hidden;
}

.landing__inner {
  max-width: 1080px;
  margin: 0 auto;
}

.landing__block,
.landing__hero,
.cta-band {
  scroll-margin-top: 88px;
}

.landing__block {
  min-height: 80svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  margin-bottom: var(--space-6);
}

.landing__block .landing__text {
  text-align: justify;
  text-align-last: center;
  margin-left: auto;
  margin-right: auto;
}

.landing__hero {
  position: relative;
  /* fill the first screen: viewport minus header (73px) */
  min-height: calc(100svh - 73px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  padding: 0;
  max-width: 880px;
  margin: 0 auto;
  text-align: center;
}

.landing__hero .landing__lead {
  margin-top: auto;
}

.landing__hero .landing__text--intro {
  margin-left: auto;
  margin-right: auto;
}

.landing__hero .landing__cta {
  justify-content: center;
}

.landing__cta {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-4);
  flex-wrap: wrap;
}

.landing__midcta {
  text-align: center;
  margin-bottom: 120px;
}

.plans__link {
  color: var(--nix-text);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.landing__lead {
  font-size: clamp(26px, 4vw, 40px);
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: -0.01em;
}

.landing__headline {
  /* same scale as the hero lead */
  font-size: clamp(26px, 4vw, 40px);
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin-bottom: var(--space-2);
}

.landing__text {
  font-size: 17px;
  font-weight: 300;
  line-height: 1.6;
  max-width: 560px;
}

.landing__text--intro {
  margin-top: var(--space-2);
  font-size: 19px;
  color: var(--nix-text-soft);
}

.landing__text + .landing__text {
  margin-top: var(--space-2);
}

body.is-flow .site-head {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-head);  /* the stack is explained where .site-head is defined — read it */
  background: var(--nix-paper);
  /* Earns its keep only as a home-screen app, where the inset is real and the
     bar does start at the top of the screen. In a Safari tab it is 0 and this
     reads as plain padding. */
  padding-top: calc(var(--space-3) + env(safe-area-inset-top, 0px));
}

/* THE ESTIMATE, AND ONLY WHERE THERE IS SOMETHING TO ESTIMATE. The scripts
   that measure the bar run at the end of the document; between first paint and
   that line this is what the layout uses, on the pages that have a bar. */
body:has(.site-head) {
  --head-h: 82px;
}

body.is-flow:has(.site-head) .page--flow {
  padding-top: var(--head-h);
}

body.is-flow .site-head::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 28px;
  pointer-events: none;
  background: linear-gradient(to bottom, var(--nix-paper), transparent);
}

.landing__lead .w {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
}

.landing__lead .w__i {
  display: inline-block;
  transform: translateY(115%);
  transition: transform var(--dur-6) var(--ease-out-expo);
  transition-delay: calc(var(--d, 0) * var(--dur-1));
}

.landing.lead-in .landing__lead .w__i {
  transform: none;
}

.landing__hero .landing__text--intro {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 760ms var(--ease-out-expo),
              transform 760ms var(--ease-out-expo);
}

.landing.lead-in .landing__hero .landing__text--intro {
  transition-delay: 320ms;
  opacity: 1;
  transform: none;
}

.landing__hero .landing__cta .btn {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur-5) var(--ease-out-expo),
              transform var(--dur-5) var(--ease-out-expo);
}

.landing.lead-in .landing__hero .landing__cta .btn:nth-child(1) { transition-delay: 480ms; }

.landing.lead-in .landing__hero .landing__cta .btn:nth-child(2) { transition-delay: var(--dur-5); }

.landing.lead-in .landing__hero .landing__cta .btn {
  opacity: 1;
  transform: none;
}

/* The header's mark waits for the preloader's traveller to land on it.
   200ms, and it is a literal because the flight it answers is timed in the
   index's own script — see the note on durations in tokens.css. */
.site-head__logo { transition: opacity 200ms ease; }

body.is-loading .site-head__logo { opacity: 0; }

/* The phone: the story keeps its shape and loses its margins. */
@media (max-width: 720px) {
  .page--flow { --flow-gutter: var(--space-3); padding: 0 var(--flow-gutter) var(--space-6); }
  .landing__hero {
    min-height: calc(100svh - 57px - 24px);
    padding: var(--space-4) 0;
  }
  .landing__block { margin-bottom: var(--space-8); }
  .landing__midcta { margin-bottom: var(--space-8); }
}

/* The blur that joins the rise, on the two both story pages carry. */
.landing__headline.reveal:not(.is-in),
.landing__midcta.reveal:not(.is-in) {
  filter: blur(5px);
}

@media (prefers-reduced-motion: reduce) {
  .landing__headline.reveal:not(.is-in),
  .landing__midcta.reveal:not(.is-in) { filter: none; }
  .landing__hero .landing__text--intro,
  .landing__hero .landing__cta .btn { opacity: 1; transform: none; transition: none; }
}

/* ── Focus ────────────────────────────────────────────────────────────────
   THE RING GOES ON EVERYTHING THAT TAKES A KEY, and it was on nine things.
   The ones below are links and cards that a keyboard reaches and a pointer
   user never notices are missing — which is exactly why they were.

   :focus-visible and not :focus, so a mouse press does not leave a ring
   behind it. The offset is a token because a ring drawn tight against a
   rounded corner reads as a broken border. */
.btn:focus-visible,
.menu-card:focus-visible,
.menu-card__go:focus-visible,
.index-foot__col a:focus-visible,
.index-foot__social a:focus-visible,
.nix-tagbar__item:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}
