/* 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 */
.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) {
  .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;
  /* NEVER SHRINK. On /agent-base and /services the bar is a child of .page,
     a flex column with a fixed height, and overflow-x: auto turns the item's
     automatic min-height into zero — so the column squeezed the pill to 20px
     while its 30px chips stuck out above and below it. */
  flex-shrink: 0;
}

/* 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.

   TWO BARS BY NAME, because they are two different things doing one job:
   .nix-sectionbar is the table of contents on /agent-base and /services, 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) .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 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.
 * -------------------------------------------------------------------- */

/* satellites ride the rings; the second rotate keeps each icon upright */

@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 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.
 * -------------------------------------------------------------------- */

.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; }

/* ── 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;
}

/* THE FOOT WITHOUT ITS COLUMNS, which is every live page since 2026-09-15.
   With the quick links gone the bar is the foot's only child, and the air
   written for a block of columns above it — the foot's own top padding plus
   the bar's margin — was a band of empty ground over a single line. The
   portal's quiet foot has its own spacing (portal.css) and is left out. */
.index-foot:not(.index-foot--quiet):not(:has(.index-foot__cols)) {
  padding-top: var(--space-4);
}

.index-foot:not(.index-foot--quiet):not(:has(.index-foot__cols)) .index-foot__bar {
  margin-top: 0;
}

/* THE COLUMNS STAY STYLED while a page still carries them: the two
   /business/ai versions nobody is served today (home.xml, home_v2.xml) do, and
   a version picked in the page picker must not arrive with bare links. */
.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;
}

/* THE WAY IN TO web.nix-ai.com, riding with the signature. Quieter than the
   copyright beside it and smaller: it is a door, not a destination, and it
   must not read as a sixth section of the site. No underline until the
   pointer is on it, which is the bargain .index-foot__col a makes above. */
.index-foot__web {
  color: var(--nix-text-quiet);
  font-size: var(--text-2xs);
  text-decoration: none;
  transition: color var(--dur-2) var(--ease-out-expo);
}

.index-foot__web:hover { color: var(--nix-text-soft); text-decoration: underline; }

/* ── THE SPANISH HALF OF A LINE, WRITTEN INTO THE MARKUP ───────────────────
   This site is bilingual by markup, not by gettext, and pages/legal.css
   already does exactly this for .legal__es. That sheet is only linked on
   /privacy, /terms and /support, though, so the class cannot be borrowed: a
   line in the foot of every page would hide nothing anywhere else. Same rule,
   its own name, in a sheet the whole site loads.

   English is what is written and the Spanish rides inside it. The head's
   language boot puts the language on <html> before first paint, so a Spanish
   reader never sees a flash of the English alone, and a reader with no script
   at all gets English — the language the markup is in. */
html:not([lang="es"]) .nix-es { display: none; }

@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 { box-shadow: none; }

/* ── 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);
}

/* ── The pricing cards, shared ───────────────────────────────────────────
   Moved out of pages/voice.css so that /my/subscriptions draws the very
   same cards the /agent-base landing sells from: one set of rules, two
   pages, nothing to drift. The class names keep their voice__ prefix
   because the landing is where they were born; portal.css only adds what
   Odoo's leg needs on top (Bootstrap's .btn, the checkout form). */

/* ── Plans: three cards, one row when there is room ───────────────────── */

/* THREE EQUAL COLUMNS, NOT AS MANY AS FIT. auto-fit with a 260px floor laid
   the cards out by the panel's width: with the rail open on a mid-sized
   screen two fitted and the third fell alone onto a second row, and the
   same cards read as different sizes on the landing and on /my/subscriptions.
   minmax(0, 1fr) so no card's content can widen its own track; one column
   below 900px, where three would be too narrow to read. */
.voice__plans {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  max-width: 960px;
  width: 100%;
  margin-top: var(--space-2);
}

/* Stacked, the cards need more air between them than side by side: at 12px
   one card's dark foot ran straight into the next one's pale top. */
@media (max-width: 900px) {
  .voice__plans { grid-template-columns: minmax(0, 1fr); gap: 24px; }
}

/* THE CARD IS A FRAME AND A SCREEN, like the feature cards on /agent-base.
   The frame is the family's vertical gradient, pale at the top through a
   colour of its own to near black, the same in both themes: an object with
   its own light. The screen is the page's card surface, drawn by ::before
   under the content, rising from the frame's bottom edge with a band of the
   gradient above it and air at the sides — which is also what makes the
   card taller than the text it holds. Everything is read off the screen, so
   the text keeps the page's own colours. isolation keeps the screen's
   z-index inside the card. */
.voice__plan {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 92px 38px 34px;
  border-radius: var(--radius-2xl);
  background: var(--plan-grad);
  overflow: hidden;
  --plan-grad: linear-gradient(180deg, #DDD9D3 0%, #BDAF9C 22%, #6B5236 52%, #2E231A 76%, #161311 100%);
}

.voice__plan::before {
  content: "";
  position: absolute;
  inset: 62px 14px 0;
  z-index: -1;
  border-radius: calc(var(--radius-2xl) * 0.8) calc(var(--radius-2xl) * 0.8) 0 0;
  background: var(--nix-surface);
}

/* One colour per card, the feature cards' order: bronze, forest, plum. */
.voice__plan:nth-child(2) {
  --plan-grad: linear-gradient(180deg, #D8DCD9 0%, #A6B6AB 22%, #3C5A4A 52%, #1F2B25 76%, #121513 100%);
}

.voice__plan:nth-child(3) {
  --plan-grad: linear-gradient(180deg, #DCD8DD 0%, #B4A8BA 22%, #54426B 52%, #271F33 76%, #141217 100%);
}
/* The card meant to be taken still has the one ink screen on the panel.
   --nix-ink flips with the theme, so the highlight is the inversion and not
   a colour the site does not have. */
.voice__plan--lead {
  color: var(--nix-on-ink);
}

.voice__plan--lead::before {
  background: var(--nix-ink);
}

.voice__plan-h {
  font-size: clamp(15px, 1.15vw, 17px);
  font-weight: 400;
  letter-spacing: -0.01em;
  line-height: 1.35;
  margin: 0;
}

/* ── Annual's saving ────────────────────────────────────────────────────
   The name on the left, what the year saves on the right, one line. The
   tag is the section bar's pill at card scale: a quiet track, no colour
   the site does not have. */
.voice__plan-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.voice__plan-tag {
  padding: 4px 12px;
  border-radius: var(--radius-pill);
  background: color-mix(in oklch, var(--nix-text) 8%, transparent);
  color: var(--nix-text);
  font-size: var(--text-xs);
  white-space: nowrap;
}

/* Baseline row: the figure and its unit on one line, so "$150" and "USD,
   once" never drift apart at any font size. */
.voice__plan-fig {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
}

.voice__plan-amount {
  font-size: 34px;
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.02em;
}

.voice__plan-per {
  font-size: var(--text-xs);
  letter-spacing: 0.02em;
  color: var(--nix-text-soft);
}

.voice__plan-p {
  color: var(--nix-text-soft);
  font-size: var(--text-sm);
  line-height: 1.5;
  margin: 0;
}

.voice__plan-life {
  color: inherit;
}

/* WHAT EVERY PLAN INCLUDES. The same eight lines on all three cards, because
   all three unlock the same Agent Base; what differs between them is how it
   is paid for, which each card already says above its list. A check drawn
   in CSS, in the card's own soft colour, so it flips on the ink card with
   everything else. */
.voice__plan-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 4px 0 0;
  padding: 12px 0 0;
  border-top: 1px solid var(--nix-rule);
}

.voice__plan-list li {
  position: relative;
  padding-left: 22px;
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--nix-text);
}

.voice__plan-list li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 0.3em;
  width: 10px;
  height: 6px;
  border-left: 1.5px solid var(--nix-text-soft);
  border-bottom: 1.5px solid var(--nix-text-soft);
  transform: rotate(-45deg);
}

.voice__plan--lead .voice__plan-list {
  border-top-color: color-mix(in oklch, var(--nix-on-ink) 22%, transparent);
}

.voice__plan--lead .voice__plan-list li {
  color: var(--nix-on-ink);
}

.voice__plan--lead .voice__plan-list li::before {
  border-color: var(--nix-on-ink-soft);
}

/* THE SCOPE OF THE LIFETIME, beside its price and before its button. Set
   apart by a rule rather than by a smaller size: it is the part of the card
   a reader has to be able to read, not a footnote. */
.voice__plan-scope {
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--nix-text-soft);
  margin: 0;
  padding-top: 12px;
  border-top: 1px solid var(--nix-rule);
}

.voice__plan--lead .voice__plan-per,
.voice__plan--lead .voice__plan-p,
.voice__plan--lead .voice__plan-scope {
  color: var(--nix-on-ink-soft);
}

.voice__plan--lead .voice__plan-life,
.voice__plan--lead .voice__seats {
  color: var(--nix-on-ink);
}

.voice__plan--lead .voice__plan-scope {
  border-top-color: color-mix(in oklch, var(--nix-on-ink) 22%, transparent);
}

/* margin-top:auto pins the button to the floor, so four lists of different
   lengths still end on one line of buttons. */
.voice__plan-cta {
  margin-top: auto;
  align-self: flex-start;
  padding: 12px 24px;
}

@media (max-width: 720px) {
  .voice__plan-cta { align-self: stretch; text-align: center; }
}
