/* NIX — /products (AB01, Agent Base OS)
   ══════════════════════════════════════════════════════════════════════════
   What only this page has. It lived in a <style> block inside the template
   until 2026-08-15: two thousand lines of stylesheet that no cache could
   keep, no tool could read beside the others, and that carried its own copy
   of every dark-theme rule.

   Everything is prefixed .ab- so it can never collide with the shared
   vocabulary, and the shared sheets still own what they own — the shell, the
   type scale, the card recipe. Sizes come off the shared ladder
   (11 / 12 / 13 / 14 / 15 / 17): picking sizes by eye per component is what
   makes a page read as a bolt-on.
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  --ab-mono: "SFMono-Regular", ui-monospace, Menlo, Consolas, monospace;
}

/* ---- Hero ---------------------------------------------------------- *
 * components.css gives .landing__hero justify-content:flex-start plus
 * .landing__lead{margin-top:auto}, which home.xml balances with a second
 * auto margin on .orbit-well. Without that counterweight every pixel of
 * free space collects above the headline and the CTA falls past the fold,
 * so this page centres the column instead. The subtraction also has to use
 * the MEASURED header height — components.css hard-codes 73px while the real bar
 * measures ~82px, which is what pushed the button off screen. */
/* Top-aligned, not centred: the product shot is meant to begin in the hero
   and continue below the fold, so the copy sits at the top and the window
   is cut by the viewport rather than shrunk to fit it. */
.landing__hero { justify-content: flex-start; padding-top: 6svh; min-height: calc(100svh - var(--head-h)); }
.landing__hero .landing__lead { margin-top: 0; font-size: clamp(32px, 5vw, 52px); }

/* Air between the hero and the first section. There is none by default:
   components.css gives .landing__block only a margin-BOTTOM, and .landing__hero
   none at all, so the two boxes touch. On this page they worse than touch —
   .ab-shot is min(600px,60svh) tall inside a hero sized to the viewport, so
   it overruns the hero's own box, and its 90px drop shadow reaches further
   still. "One idea, many jobs." was landing inside that bleed.
   Scoped to the FIRST block rather than to .landing__block: only the
   hero/first-section seam has the overrun, and every later block is already
   separated by the previous one's margin-bottom. It used to be #jobs by id,
   which broke the moment #jobs was hidden — the air went with it and the
   hero's bleed landed on whatever came next. :first-of-type counts
   sections, and the hero is a <header>, so it never wins this. */
.landing__inner > section.landing__block:first-of-type { margin-top: clamp(var(--space-8), 10svh, 140px); }
/* Why, How and What are the page's three chapters and they get the hero
   seam's air, not .landing__block's 48px — the same distance between every
   pair of them, so the eye reads three parts rather than a list with one
   unusually large gap in it at the top. */
.landing__block:has(> .ab-chapter) { margin-top: clamp(var(--space-8), 10svh, 140px); }

/* nowrap on the name: without it "AGENT BASE OS" breaks onto three lines
   beside the code at every desktop width. Caught by looking at the page. */
.ab-mark { display: flex; align-items: baseline; justify-content: center; gap: 12px; margin-bottom: var(--space-3); flex-wrap: wrap; }
.ab-mark__name { white-space: nowrap; }
/* A rung off the shared type ladder, and deliberately BELOW .landing__lead
   (clamp(26px,4vw,40px)): at clamp(30px,5vw,46px) the product code rendered
   larger than the sentence selling the product. */
.ab-mark__id { font-size: clamp(22px, 3vw, 32px); font-weight: 300; letter-spacing: 0.06em; line-height: 1; }
.ab-mark__sep { width: 1px; height: 26px; background: var(--nix-rule); align-self: center; }
.ab-mark__name { font-size: var(--text-2xs); letter-spacing: 0.18em; text-transform: uppercase; color: var(--nix-text-soft); }
/* The whole entrance ladder is rebuilt here as animations. components.css stages
   the hero with TRANSITIONS keyed to .landing.lead-in (intro 320ms, buttons
   480/640ms), which works on home.xml because nix.js adds that class after
   load. This page ships .lead-in in the markup — it has to, or the intro
   and the buttons never leave opacity:0 — and a transition does not fire on
   an element's first style computation. So those rungs would simply paint
   at full opacity on frame one while the headline rose above them.
   The intro splits this ladder in two. The dashboard now enters on its own
   cue (shot-in) a long time before the words do (lead-in), so the two can
   no longer share one class.

   Nothing here needs to fight a running animation to stay hidden: without
   lead-in, components.css already leaves the lead, the intro line and both buttons
   at opacity 0 — that is exactly why the class had to ship in the markup
   before. .ab-mark and .ab-shotwrap are this page's own, so they are the
   only two that need an explicit resting state — plus .landing__lead, which
   components.css does not park at 0 (its comment names the intro line and the
   buttons, not the headline), so without this it reads out over an empty
   hero all through the intro. Verified by rendering the stage, not by
   trusting the comment. */
/* Was `opacity: 0` — the intro's resting state. The intro is gone; see the
   NO ENTRANCE block at the foot of this file. */
.ab-mark, .ab-shotwrap, .landing__lead { opacity: 1; }

/* During the intro the hero's words are hidden but still hold their space,
   so the mock sat low with a screenful of nothing above it. JS lifts it to
   the middle of the viewport for the intro and puts it back at the end,
   which is what makes it settle into place as the words arrive.

   The lift is written straight onto the element as an inline transform, and
   only the transition lives here. An inherited custom property read by a
   rule was the obvious way to do it and it cost hours: the value has to
   survive inheritance, a filling animation on the same element, and a
   transition, and when it did not the page simply rendered at 0 with no
   error anywhere. An inline style outranks all of it and depends on none
   of it. The animation no longer touches transform either, so nothing but
   these two lines ever decides where the mock sits. */
/* The entrance animates opacity ONLY, and the position is always the plain
   rule below. They used to share transform: the animation filled 'both' and
   held it, then .shot-set switched the animation off to hand the transform
   over — and at that hand-off the value dropped to 0 and crawled back to
   --lift across the 900ms transition. A visible fall and climb, every load.
   Sampling the computed transform every 200ms is what found it; reading the
   rule looked right, and so did a synchronous test of the same mechanism in
   isolation, because getComputedStyle reports the transition's target.

   Two declarations can only fight over one property if they share it. Now
   nothing but this rule ever writes transform, so there is no hand-off. */
/* The camera. One transform on one element, interpolated — so the close-up
   on the card and the wide shot of the chat are the same move rather than
   two states swapped. transform-origin is set once and left alone for the
   whole intro: the pull-back ends at scale 1, where the origin stops
   mattering, so it can be cleared at the end without anything jumping. */
.ab-shotwrap { transition: transform 1150ms cubic-bezier(.65,.02,.16,1); }
.ab-shotwrap.no-anim { transition: none; }

.landing.shot-in .ab-shotwrap { animation: ab-shot-fade var(--dur-6) var(--ease-out-expo) both; }
@keyframes ab-shot-fade { from { opacity: 0; } to { opacity: 1; } }
.landing.lead-in .ab-mark,
.landing.lead-in .landing__lead,
.landing.lead-in .landing__hero .landing__text--intro,
.landing.lead-in .landing__hero .landing__cta .btn { animation: ab-rise var(--dur-6) var(--ease-out-expo) both; }
.landing.lead-in .landing__lead { animation-delay: var(--dur-2); }
.landing.lead-in .landing__hero .landing__text--intro { animation-delay: 320ms; }
.landing.lead-in .landing__hero .landing__cta .btn:first-child { animation-delay: 480ms; }
.landing.lead-in .landing__hero .landing__cta .btn:last-child { animation-delay: var(--dur-5); }
@keyframes ab-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

/* Download pills: glyph + label on one line. The disabled Windows one
   keeps the pill shape at half voice with its own Coming soon whisper. */
.btn--dl { display: inline-flex; align-items: center; gap: 10px; }
.btn--dl svg { width: 16px; height: 16px; flex: none; }
/* The Windows pill sits switched off; the chip rides ON TOP of it and
   outside the dimming, so the promise reads at full voice. */
.btn-soon-wrap { position: relative; display: inline-flex; }

/* The floor, under the button it is about. Centred under both buttons it read
   as a third line of the pitch; it is not a pitch, it is the answer to "will
   this run on mine", and it should be found by someone looking for it rather
   than land on someone who is not. Quiet, small, and directly beneath the
   only button it describes. */
.btn-dl-wrap { display: inline-flex; flex-direction: column; align-items: center; gap: 7px; }
.landing__hero .landing__cta { align-items: flex-start; }
.btn__req { font-style: normal; font-size: 11px; letter-spacing: 0.02em;
  line-height: 1.3; color: var(--nix-text-quiet); opacity: 0.7; }
.btn--soon { opacity: 0.35; cursor: default; }
.btn__soon { position: absolute; top: -9px; right: 14px; font-style: normal;
  font-size: 10px; letter-spacing: 0.04em; padding: 3px 8px;
  border-radius: var(--radius-pill); background: var(--nix-ink); color: var(--nix-on-ink); }

/* ---- Hero device: the Agent Base dashboard -------------------------- *
 * A replica of the real app's chrome, drawn in markup and non-functional:
 * the rail (mark, clock, agent avatars with running dots, bell, settings,
 * account), the header with the NIX spark, a scrolling agent grid whose
 * cards show their machine actually working, and the NIX panel with a
 * conversation and its session history open.
 *
 * The three big surfaces are Agent Base's own measured tokens and follow
 * the site's theme. Every small fill is a neutral grey: a themed token
 * resolved to its dark value on a light page in an earlier draft, and
 * rgba(128,128,128,a) composites correctly against both grounds. The card
 * contents are stylised app layouts, not any real product's screen. */
.ab-shotwrap { width: 100vw; margin-top: var(--space-6); display: flex; justify-content: center; }
.ab-shot {
  --s-canvas: #f5f5f5; --s-page: #fafafa; --s-panel: #ffffff;
  --s-text: #191919; --s-accent: #5568ea;
  width: min(1360px, 96vw); height: min(600px, 60svh); border-radius: 18px;
  background: var(--s-canvas); color: var(--s-text); text-align: left;
  /* Two tracks: the agents panel and the NIX panel. There used to be a
     56px rail in front of them; it is hidden at the markup, and its track
     had to go with it or the grid would keep an empty 56px gutter. */
  /* One track. The 320px second column held the NIX panel, hidden at the
     markup like the rail before it; leaving the track behind would keep an
     empty 320px gutter beside the agents. */
  display: grid; grid-template-columns: 1fr; gap: 7px; padding: 7px;
  box-shadow: 0 2px 6px rgba(26,26,26,.08), 0 44px 90px -34px rgba(26,26,26,.36);
  font-size: 12px; overflow: hidden; }
html[data-theme="dark"] .ab-shot {
  --s-canvas: #0e0e0e; --s-page: #161616; --s-panel: #1a1a1a;
  --s-text: #f5f5f5; --s-accent: #758af0;
  box-shadow: 0 2px 6px rgba(0,0,0,.5), 0 44px 90px -34px rgba(0,0,0,.72); }

.ab-shot svg { display: block; }

/* rail */
.ab-shot__rail { background: var(--s-page); border-radius: 13px; padding: 10px 0 12px;
  display: flex; flex-direction: column; align-items: center; gap: 9px; }
.ab-shot__logo { display: flex; align-items: center; justify-content: center; width: 32px; height: 28px; }
.ab-shot__logo img { width: 24px; height: 24px; }
html[data-theme="dark"] .ab-shot__logo img { filter: brightness(0) invert(1); }

.ab-shot__clock { width: 32px; border-radius: var(--radius-sm); background: rgba(128,128,128,0.14);
  padding: 4px 0; display: flex; flex-direction: column; align-items: center; gap: 1px;
  font-size: var(--text-2xs); font-weight: 400; font-variant-numeric: tabular-nums;
  line-height: 1.15; color: var(--s-text); }
.ab-shot__clock span { font-weight: 400; }
.ab-shot__rule { width: 24px; height: 1px; background: rgba(128,128,128,0.24); }
.ab-shot__av { position: relative; width: 32px; height: 32px; border-radius: 9px;
  background: rgba(128,128,128,0.14); display: flex; align-items: center; justify-content: center;
  font-size: 12px; }
.ab-shot__av i { position: absolute; right: -2px; bottom: -2px; width: 7px; height: 7px;
  border-radius: 50%; background: var(--s-accent); box-shadow: 0 0 0 2px var(--s-page); }
.ab-shot__rspace { margin-top: auto; }
.ab-shot__ricon { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
  color: var(--s-text); opacity: 0.5; }
.ab-shot__ricon svg { width: 16px; height: 16px; }
.ab-shot__me { width: 26px; height: 26px; border-radius: 50%; font-size: var(--text-2xs);
  box-shadow: inset 0 0 0 1.2px rgba(128,128,128,0.4);
  display: flex; align-items: center; justify-content: center; }

/* main */
.ab-shot__main { background: var(--s-page); border-radius: 13px; padding: 12px 14px;
  min-width: 0; min-height: 0; display: flex; flex-direction: column; }
.ab-shot__head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.ab-shot__title { font-size: 21px; letter-spacing: -0.01em; }
.ab-shot__spark { margin-left: auto; color: var(--s-text); opacity: 0.85; }
.ab-shot__spark svg { width: 19px; height: 19px; }
/* the grid scrolls in place, the way the real one does once you have more
   machines than fit */
.ab-shot__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;
  overflow-y: auto; padding-right: 2px; }

.ab-shot__new { border-radius: var(--radius-chip); background: rgba(128,128,128,0.10); aspect-ratio: 16 / 10;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  color: var(--s-text); }
.ab-shot__new i { font-style: normal; font-size: 20px; font-weight: 300; line-height: 1; }
.ab-shot__new span { font-size: 12px; opacity: 0.8; }

.ab-shot__card { position: relative; border-radius: var(--radius-chip); overflow: hidden; aspect-ratio: 16 / 10;
  background: rgba(128,128,128,0.10); }
/* above the capture: .ab-shot__real is z-index 1, and without a stacking
   order of its own the name was painted underneath the screenshot. */
.ab-shot__cbar { position: absolute; left: 0; right: 0; bottom: 0; z-index: 3; display: flex;
  align-items: center; gap: 8px; padding: 8px 10px; color: var(--s-text); font-size: 12.5px; }

/* what the machine is looking at — stylised app layouts, not screenshots
   of anyone's product */
.ab-shot__prev { position: absolute; inset: 0; display: flex; flex-direction: column;
  background: var(--s-panel); }
.ab-shot__chrome { display: flex; align-items: center; gap: 4px; padding: 6px 8px;
  background: rgba(128,128,128,0.12); }
.ab-shot__chrome i { width: 5px; height: 5px; border-radius: 50%; background: rgba(128,128,128,0.45); }
.ab-shot__chrome b { flex: 1; height: 7px; margin-left: 5px; border-radius: 4px;
  background: rgba(128,128,128,0.22); }
.ab-shot__body { flex: 1; padding: 8px; display: flex; flex-direction: column; gap: 5px; min-height: 0; }
.ab-shot__row { display: flex; align-items: center; gap: 5px; }
.ab-shot__row i { height: 6px; border-radius: 3px; background: rgba(128,128,128,0.26); }
.ab-shot__row i.f { flex: 1; }
.ab-shot__row i.n { width: 26px; background: rgba(128,128,128,0.36); }
.ab-shot__row i.g { width: 26px; background: #4a9d6a; opacity: 0.7; }
.ab-shot__row i.a { width: 34px; background: var(--s-accent); opacity: 0.75; }
.ab-shot__hero { height: 42%; border-radius: 5px; background: rgba(128,128,128,0.22); }
.ab-shot__btn { height: 11px; width: 46%; border-radius: 6px; background: var(--s-accent); opacity: 0.8; }
.ab-shot__gut { display: flex; gap: 6px; flex: 1; min-height: 0; }
.ab-shot__gut u { width: 8px; border-radius: 2px; background: rgba(128,128,128,0.18); text-decoration: none; }
.ab-shot__gut s { flex: 1; display: flex; flex-direction: column; gap: 4px; text-decoration: none; }

/* NIX panel */
.ab-shot__panel { position: relative; background: var(--s-panel); border-radius: 13px; padding: 12px;
  display: flex; flex-direction: column; min-height: 0; }
.ab-shot__phead { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; color: var(--s-text); }
.ab-shot__phead svg { width: 15px; height: 15px; opacity: 0.6; }
.ab-shot__ptitle { flex: 1; text-align: center; font-size: 14px; }
.ab-shot__chat { display: flex; flex-direction: column; gap: 9px; overflow: hidden; min-height: 0; }
.ab-shot__msg { font-size: 12px; line-height: 1.5; border-radius: var(--radius-chip); padding: 8px 11px; max-width: 92%; }
.ab-shot__msg--me { align-self: flex-end; background: rgba(128,128,128,0.14); }
.ab-shot__msg--nix { background: transparent; padding-left: 0; opacity: 0.8; }
.ab-shot__made { display: inline-flex; align-items: center; gap: 6px; align-self: flex-start;
  font-size: var(--text-2xs); border-radius: var(--radius-pill); padding: 5px 10px; background: rgba(128,128,128,0.12); }
.ab-shot__made em { font-style: normal; width: 6px; height: 6px; border-radius: 50%;
  background: var(--s-accent); }
/* The session list REPLACES the conversation rather than floating over it:
   pressing history swaps the panel into a list view, picking a row swaps it
   back with that chat loaded. A card hovering above the messages read as a
   modal and hid the thing it indexes. */
.ab-shot__hist { display: none; flex: 1; min-height: 0; flex-direction: column; gap: 2px; }
.ab-shot__panel.is-listing .ab-shot__hist { display: flex; }
.ab-shot__panel.is-listing .ab-shot__chat { display: none; }
.ab-shot__hist span { font-size: 12.5px; padding: 10px 10px; border-radius: var(--radius-sm); opacity: 0.8; }
.ab-shot__hist span.h { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  opacity: 0.45; padding-bottom: 4px; }
.ab-shot__hist span.on { background: rgba(128,128,128,0.16); opacity: 1; }
.ab-shot__ask { margin-top: auto; display: flex; align-items: center; gap: 9px;
  border-radius: var(--radius-pill); background: rgba(128,128,128,0.12); padding: 7px 7px 7px 13px; }
.ab-shot__ask svg { width: 14px; height: 14px; opacity: 0.6; }
.ab-shot__ph { flex: 1; font-size: 12.5px; opacity: 0.5; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.ab-shot__ph.is-typing { opacity: 1; }
.ab-shot__ph.is-typing::after { content: ''; display: inline-block; width: 1.5px; height: 1em;
  background: currentColor; vertical-align: -2px; margin-left: 1px; animation: ab-caret 1.05s step-end infinite; }
.ab-shot__send { width: 28px; height: 28px; border-radius: 50%; background: var(--s-text);
  color: var(--s-canvas); display: flex; align-items: center; justify-content: center; }

/* A FOURTH BREAKPOINT, and it earns it: this is the only grid on the site
   whose cells are screenshots of a desktop app. Three of them stop being
   readable well before the layout stops fitting, which is what 900 measures.
   Two tracks from 1100 down, one from 900 — see the note in tokens.css. */
@media (max-width: 1100px) { .ab-shot__grid { grid-template-columns: repeat(2, 1fr); } }
/* ---- The phone, as the app actually does it ------------------------- *
 * Taken from the app's own responsive.css at its own breakpoint, 640px:
 * the rail is hidden and a fixed BOTTOM TAB BAR of four icon buttons —
 * agents, notifications, settings, perfil — becomes the navigation, the
 * content reserves the bar's height, and the side panel stops being a
 * push-from-the-right and becomes a full-screen sheet that rises from the
 * bottom. Until now this mock just squeezed the desktop, which is the one
 * thing the app deliberately does not do at this width.
 *
 * 720 rather than the mock's own 900: it is the app's number, and the
 * point is to show what the app shows. Between the two the mock keeps its
 * older narrow layout, which is a fair picture of the app's own 641-1024
 * band. */
.ab-shot__tabs { display: none; }

@media (max-width: 640px) {
  .ab-shot__grid { grid-template-columns: 1fr; }
  .ab-shot__title { font-size: 17px; }
}

/* ---- Generic device frame ------------------------------------------ *
 * The .plan recipe /services uses, so every device sits on the same surface
 * as the home page's cards. */
/* Three measures on this page and no more: 560px for body copy (the shared
   own .landing__text), 720px for devices (matching .faq), 860px for the two
   wide grids. Five different widths made the page's edges step in and out
   four times on the way down. */
.ab-dev { width: min(720px, 100%); margin: var(--space-8) auto 0; background: var(--nix-surface);
  border-radius: var(--radius-xl); padding: var(--space-4); text-align: left;
  box-shadow: var(--lift-2); }
/* Not every device needs a card under it. The layer stack is already a
   stack of surfaces, and the blast drawing earns the full column — six
   identical 720px cards in a row is what makes a page read as a list. */
.ab-dev__top { display: flex; align-items: center; gap: 10px; margin-bottom: var(--space-3); }
.ab-dev__title { font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--nix-text-soft); }

.ab-stage { font-size: var(--text-2xs); letter-spacing: 0.14em; text-transform: uppercase; color: var(--nix-text-soft);
  border: 1px solid var(--nix-rule); border-radius: var(--radius-pill); padding: 6px 13px; display: inline-block; }
.ab-stage + .landing__headline { margin-top: var(--space-3); }

/* The chapter the section belongs to — Why this thing exists, How it
   starts — written as a heading, not as an eyebrow: page type, full text
   colour, no uppercase and no tracking. It is the section's real title and
   the line under it is the sentence that answers it, so it sits one step
   above the headline in size rather than one step below it in weight.
   Alignment is inherited, which is what lets the same class sit centred
   over "Easy to start." and left over the two-column .ab-what. */
.ab-chapter { display: block; font-size: clamp(34px, 5.4vw, 56px);
  font-weight: 400; line-height: 1.1; letter-spacing: -0.015em;
  color: var(--nix-text); margin: 0 0 var(--space-2); }
/* Wherever a chapter titles a section, that section's headline is the
   subordinate line: same face and same colour, one size down. Keyed off
   the section rather than off adjacency — "Why" sits at the top of #what
   with a two-column block between it and its headline, and a sibling
   selector would have left that one line at full size while the identical
   arrangement under "How" stepped down. Softening it too would leave the
   section no white line at all; .landing__text is already soft. */
.landing__block:has(> .ab-chapter) .landing__headline {
  font-size: clamp(21px, 2.6vw, 28px); }
/* A chapter that titles the whole block stands off it. One that sits
   directly on its headline keeps the tight pair. */
.ab-chapter + :not(.landing__headline) { margin-top: var(--space-6); }
/* .ab-what now shares the How section with the flow below it, and the two
   are separate arguments — the gap has to read as bigger than the one
   inside either. */
/* The seam .ab-what + .landing__headline used to hold is a section
   boundary now — the chapter rule below spaces it. */

/* The three reasons, under Why. Three cards on the page's own ladder — the
   same white-paper-and-shadow in light and #1a1a1a in dark that the frames
   below use — and the same 12px gap and 25px radius as .ab-bento, so this
   reads as the section that sets up the bento rather than a different kind
   of thing. */
.ab-why { width: min(1190px, 100%); margin: var(--space-6) auto 0;
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
  text-align: left; }
.ab-why__item { background: #ffffff; border-radius: var(--radius-2xl);
  padding: var(--space-4);
  /* A column, and the number pushed to the top by the copy's auto margin:
     the three cards then agree on where the number sits and where the text
     starts, whatever the line count is. */
  display: flex; flex-direction: column; min-height: 230px;
  box-shadow: var(--lift-1); }
html[data-theme="dark"] .ab-why__item { background: #1a1a1a; box-shadow: none; }

/* The count, in the page's own label style. It is the only thing here that
   says these are three of something, now that the sentences are one line
   each and no longer say it themselves. */
.ab-why__no { font-size: var(--text-2xs); letter-spacing: 0.18em; color: var(--nix-text-soft);
  margin: 0 0 auto; }
.ab-why__title { font-size: clamp(17px, 1.4vw, 21px); font-weight: 400;
  margin: var(--space-4) 0 var(--space-1); }
.ab-why__text { font-size: 14px; line-height: 1.6; color: var(--nix-text-soft);
  margin: 0; }

/* The grid carries .reveal for the observer, but hands the entrance to its
   children — .ab-what does the same. Left on the grid, all three cards
   fade as one block and the count stops meaning anything. */
.ab-why.reveal { opacity: 1; transform: none; transition: none; }
.ab-why__item { opacity: 0; transform: translateY(14px);
  transition: opacity 720ms var(--ease-out-expo),
              transform 720ms var(--ease-out-expo); }
.ab-why.is-in .ab-why__item { opacity: 1; transform: none; }
.ab-why.is-in .ab-why__item:nth-child(2) { transition-delay: var(--dur-1); }
.ab-why.is-in .ab-why__item:nth-child(3) { transition-delay: 180ms; }

/* ---- the shot, alive ------------------------------------------------ *
 * The cards do not sit still: their content scrolls, rows light in turn as
 * work lands, a caret blinks in the editors and a pointer moves across.
 * All of it is CSS on a loop rather than nine embedded players — a page
 * arguing for isolation should not load third-party trackers into its own
 * front door, and a stock video is not what the agent is doing anyway. */
.ab-shot__prev { overflow: hidden; }
/* Drop-in slot for a real recording or screenshot of Agent Base: give the
   card an <img class="ab-shot__real"> (or a <video>) and it covers the
   drawn mock without touching anything else. */
.ab-shot__real { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: top center; z-index: 1; }
/* On a real capture the name has no ground of its own, so it gets a scrim.
   Light text alone was invisible over a white page — and it read as fine in
   dark only because --s-text is already white there. */
.ab-shot__card:has(.ab-shot__real) .ab-shot__cbar { color: #f5f5f5; padding-top: 20px;
  background: linear-gradient(to top, rgba(0,0,0,.78), rgba(0,0,0,.35) 55%, rgba(0,0,0,0)); }
.ab-shot__body { position: relative; }
.ab-shot.is-live .ab-shot__body { animation: ab-scan 14s ease-in-out infinite; }
.ab-shot.is-live .ab-shot__card:nth-child(3n) .ab-shot__body { animation-duration: 18s; animation-delay: -4s; }
.ab-shot.is-live .ab-shot__card:nth-child(3n+1) .ab-shot__body { animation-duration: 16s; animation-delay: -9s; }
@keyframes ab-scan {
  0%, 14%   { transform: translateY(0); }
  46%, 60%  { transform: translateY(-14%); }
  88%, 100% { transform: translateY(0); }
}
.ab-shot.is-live .ab-shot__row i.f { animation: ab-land 6s ease-in-out infinite; }
.ab-shot.is-live .ab-shot__row:nth-child(2) i.f { animation-delay: .5s; }
.ab-shot.is-live .ab-shot__row:nth-child(3) i.f { animation-delay: 1s; }
.ab-shot.is-live .ab-shot__row:nth-child(4) i.f { animation-delay: 1.5s; }
.ab-shot.is-live .ab-shot__row:nth-child(5) i.f { animation-delay: 2s; }
@keyframes ab-land { 0%, 40% { opacity: .45; } 55%, 95% { opacity: 1; } 100% { opacity: .45; } }
.ab-shot.is-live .ab-shot__row i.a { animation: ab-caret 1.1s step-end infinite; }
@keyframes ab-caret { 50% { opacity: .25; } }
/* A named cursor, the way a multiplayer canvas draws one: an arrow with a
   label riding behind it. The label is the agent's own name, so the grid
   reads as eight machines each being driven by someone, which is the whole
   claim of the product. Colour is the one place this page uses any, and it
   is inside the product mock only — the page around it stays monochrome.

   Every cursor gets its OWN path. They used to share one keyframe and vary
   only duration, which reads as one animation played at eight speeds. */
.ab-shot__ptr { position: absolute; left: 10%; top: 18%; width: 14px; height: 14px;
  pointer-events: none; opacity: 0; z-index: 2; --c: #4a86f7; }
.ab-shot__ptr svg { width: 14px; height: 14px; display: block; fill: var(--c);
  stroke: #fff; stroke-width: 1.6; stroke-linejoin: round;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,.35)); }
/* the name pill sits below-right of the tip, as the reference does */
.ab-shot__ptr b { position: absolute; left: 11px; top: 11px; background: var(--c);
  color: #fff; font-family: var(--font-sans); font-weight: 400; font-size: 9px;
  line-height: 1; letter-spacing: .01em; padding: 4px 7px 5px; border-radius: 9px;
  white-space: nowrap; box-shadow: 0 1px 3px rgba(0,0,0,.25); }
/* a yellow pill needs dark text to stay legible */
.ab-shot__card:nth-child(4) .ab-shot__ptr b { color: #1a1a1a; }
.ab-shot__ptr i { position: absolute; left: -6px; top: -6px; width: 26px; height: 26px;
  border-radius: 50%; border: 1.5px solid var(--c); opacity: 0; }

.ab-shot.is-live .ab-shot__ptr { animation: ab-ptr-a 11s ease-in-out infinite; }
.ab-shot.is-live .ab-shot__ptr i { animation: ab-click 11s ease-in-out infinite; }

/* one colour and one path per machine; :nth-child(1) is the New agent tile */
.ab-shot__card:nth-child(2) .ab-shot__ptr { --c: #4a86f7; }
.ab-shot__card:nth-child(3) .ab-shot__ptr { --c: #4caf6d; }
.ab-shot__card:nth-child(4) .ab-shot__ptr { --c: #f2c14e; }
.ab-shot__card:nth-child(5) .ab-shot__ptr { --c: #8b5cf6; }
.ab-shot__card:nth-child(6) .ab-shot__ptr { --c: #e8674c; }
.ab-shot__card:nth-child(7) .ab-shot__ptr { --c: #21b0c4; }
.ab-shot__card:nth-child(8) .ab-shot__ptr { --c: #d2588f; }
.ab-shot__card:nth-child(9) .ab-shot__ptr { --c: #6b7fd7; }

.ab-shot.is-live .ab-shot__card:nth-child(2) .ab-shot__ptr,
.ab-shot.is-live .ab-shot__card:nth-child(2) .ab-shot__ptr i { animation-duration: 11s; animation-delay: -2s; }
.ab-shot.is-live .ab-shot__card:nth-child(2) .ab-shot__ptr { animation-name: ab-ptr-a; }
.ab-shot.is-live .ab-shot__card:nth-child(3) .ab-shot__ptr,
.ab-shot.is-live .ab-shot__card:nth-child(3) .ab-shot__ptr i { animation-duration: 13s; animation-delay: -5s; }
.ab-shot.is-live .ab-shot__card:nth-child(3) .ab-shot__ptr { animation-name: ab-ptr-b; }
.ab-shot.is-live .ab-shot__card:nth-child(4) .ab-shot__ptr,
.ab-shot.is-live .ab-shot__card:nth-child(4) .ab-shot__ptr i { animation-duration: 15s; animation-delay: -9s; }
.ab-shot.is-live .ab-shot__card:nth-child(4) .ab-shot__ptr { animation-name: ab-ptr-c; }
.ab-shot.is-live .ab-shot__card:nth-child(5) .ab-shot__ptr,
.ab-shot.is-live .ab-shot__card:nth-child(5) .ab-shot__ptr i { animation-duration: 12s; animation-delay: -3s; }
.ab-shot.is-live .ab-shot__card:nth-child(5) .ab-shot__ptr { animation-name: ab-ptr-d; }
.ab-shot.is-live .ab-shot__card:nth-child(6) .ab-shot__ptr,
.ab-shot.is-live .ab-shot__card:nth-child(6) .ab-shot__ptr i { animation-duration: 14s; animation-delay: -7s; }
.ab-shot.is-live .ab-shot__card:nth-child(6) .ab-shot__ptr { animation-name: ab-ptr-a; }
.ab-shot.is-live .ab-shot__card:nth-child(7) .ab-shot__ptr,
.ab-shot.is-live .ab-shot__card:nth-child(7) .ab-shot__ptr i { animation-duration: 16s; animation-delay: -4s; }
.ab-shot.is-live .ab-shot__card:nth-child(7) .ab-shot__ptr { animation-name: ab-ptr-b; }
.ab-shot.is-live .ab-shot__card:nth-child(8) .ab-shot__ptr,
.ab-shot.is-live .ab-shot__card:nth-child(8) .ab-shot__ptr i { animation-duration: 12.5s; animation-delay: -8s; }
.ab-shot.is-live .ab-shot__card:nth-child(8) .ab-shot__ptr { animation-name: ab-ptr-c; }
.ab-shot.is-live .ab-shot__card:nth-child(9) .ab-shot__ptr,
.ab-shot.is-live .ab-shot__card:nth-child(9) .ab-shot__ptr i { animation-duration: 13.5s; animation-delay: -6s; }
.ab-shot.is-live .ab-shot__card:nth-child(9) .ab-shot__ptr { animation-name: ab-ptr-d; }

/* a: reads a page top-down, then jumps back up */
@keyframes ab-ptr-a {
  0%       { transform: translate(0, 0);        opacity: 0; }
  6%       { opacity: .95; }
  24%      { transform: translate(210%, 140%);  opacity: .95; }
  34%, 40% { transform: translate(210%, 140%); }
  58%      { transform: translate(520%, 60%);   opacity: .95; }
  66%, 72% { transform: translate(520%, 60%); }
  88%      { transform: translate(120%, 330%);  opacity: .8; }
  100%     { transform: translate(0, 0);        opacity: 0; }
}
/* b: works a wide row left to right, low on the page */
@keyframes ab-ptr-b {
  0%       { transform: translate(430%, 300%);  opacity: 0; }
  8%       { opacity: .95; }
  22%      { transform: translate(60%, 250%);   opacity: .95; }
  30%, 37% { transform: translate(60%, 250%); }
  55%      { transform: translate(300%, 40%);   opacity: .95; }
  63%, 70% { transform: translate(300%, 40%); }
  86%      { transform: translate(600%, 210%);  opacity: .8; }
  100%     { transform: translate(430%, 300%);  opacity: 0; }
}
/* c: short, hesitant hops around one corner */
@keyframes ab-ptr-c {
  0%       { transform: translate(90%, 40%);    opacity: 0; }
  7%       { opacity: .95; }
  19%      { transform: translate(340%, 90%);   opacity: .95; }
  28%, 34% { transform: translate(340%, 90%); }
  46%      { transform: translate(250%, 260%);  opacity: .95; }
  54%, 61% { transform: translate(250%, 260%); }
  74%      { transform: translate(470%, 320%);  opacity: .95; }
  82%, 89% { transform: translate(470%, 320%); }
  100%     { transform: translate(90%, 40%);    opacity: 0; }
}
/* d: one long diagonal sweep, then straight back */
@keyframes ab-ptr-d {
  0%       { transform: translate(560%, 30%);   opacity: 0; }
  9%       { opacity: .95; }
  31%      { transform: translate(80%, 330%);   opacity: .95; }
  41%, 49% { transform: translate(80%, 330%); }
  68%      { transform: translate(520%, 250%);  opacity: .95; }
  77%, 84% { transform: translate(520%, 250%); }
  100%     { transform: translate(560%, 30%);   opacity: 0; }
}
/* the ring fires exactly where the pointer stops */
@keyframes ab-click {
  0%, 33%   { opacity: 0; transform: scale(.4); }
  36%       { opacity: .7; transform: scale(.4); }
  44%       { opacity: 0;  transform: scale(1.1); }
  65%       { opacity: 0;  transform: scale(.4); }
  68%       { opacity: .7; transform: scale(.4); }
  76%, 100% { opacity: 0;  transform: scale(1.1); }
}

/* ---- Intro staging ---------------------------------------------------
   The mock builds itself before the words arrive: frame and rail, then the
   NIX panel, then the request is typed and sent, then the agents appear.
   Only the panel and the grid are staged here — the frame rides in on
   .landing.shot-in above, and the rail is simply part of the frame. */
/* Phase one is the chat and nothing else. The rail, the agents and the
   frame all keep their place in the layout — they are hidden with opacity,
   not removed — so when they arrive nothing reflows and the panel does not
   jump. The mock's own surface goes transparent meanwhile, which is what
   leaves a single chat panel floating on the page.

   translateX belongs to --panel-x and to nothing else. The entrance used to
   own it too, and then the centring rule (0,3,0) lost to the entrance's
   end state (0,4,0) and the panel never moved. Separating the two — opacity
   for the entrance, the variable for the position — means they cannot
   fight, whatever order they happen in. */
.ab-shot { transition: background-color 620ms var(--ease-out-expo),
                       box-shadow 620ms var(--ease-out-expo),
                       gap 820ms var(--ease-in-out),
                       grid-template-columns 820ms var(--ease-in-out); }
/* the panels arrive a beat after their columns start opening, so they are
   never seen squashed into a sliver */
.ab-shot__rail, .ab-shot__main { transition: opacity 520ms var(--ease-out-expo) 180ms; }

/* .solo needs (0,2,1) to match html[data-theme="dark"] .ab-shot, which also
   sets box-shadow. At plain (0,2,0) it lost, and the dark theme kept drawing
   the frame's shadow around a chat that was supposed to be floating alone. */
html .ab-shot.solo { background: transparent; box-shadow: none; }
.ab-shot.solo .ab-shot__rail,
.ab-shot.solo .ab-shot__main { opacity: 0; transition-delay: 0ms; }
/* At full width the composer stretched the whole way across and stopped
   reading as an input. Half, centred, and it settles back to the panel's
   own width as the columns close in around it. */
.ab-shot__ask, .ab-shot__chat { transition: width 820ms var(--ease-in-out);
  width: 100%; margin-left: auto; margin-right: auto; }
.ab-shot.solo .ab-shot__ask,
.ab-shot.solo .ab-shot__chat { width: 50%; }
/* ---- Phase one: the composer, larger ---------------------------------
   Not a different component — the same bar, the same pill, sitting where
   it always sits, just bigger. That is what makes the send smooth: nothing
   is swapped for anything, so there is no cut to hide. The bar shrinks to
   its normal size while the chat around it fades up, and because it never
   moved in the layout the camera can pull straight back onto it.

   An earlier pass built a separate card with its own grid layout, and no
   amount of dissolving made that read as one shot — a centred card and a
   bar at the foot of a panel are two different places, and the eye sees
   the jump however you fade it. */
.ab-shot.compose .ab-shot__phead,
.ab-shot.compose .ab-shot__chat { opacity: 0; }
/* the panel is only a container while the bar carries the shot */
.ab-shot.compose .ab-shot__panel { background: transparent; box-shadow: none; }

/* Sized so the whole sentence still fits on one line. The placeholder
   inherits nowrap with an ellipsis, so a larger face would have clipped
   the request mid-word — the camera is already magnifying this, so the
   type does not have to. */
.ab-shot.compose .ab-shot__ask { min-height: 64px; gap: 14px;
  padding: 12px 12px 12px 24px; align-items: center; }
.ab-shot.compose .ab-shot__ask > svg { width: 17px; height: 17px; }
.ab-shot.compose .ab-shot__send { width: 40px; height: 40px; }
.ab-shot.compose .ab-shot__ph { font-size: 15px; }

/* everything the bar changes on the way back down, so the return is a
   resize rather than a replacement */
.ab-shot__ask { transition: width 820ms var(--ease-in-out),
  min-height 820ms var(--ease-in-out), padding 820ms var(--ease-in-out),
  gap 820ms var(--ease-in-out); }
.ab-shot__ph { transition: font-size 820ms var(--ease-in-out); }
.ab-shot__send { transition: width 820ms var(--ease-in-out),
  height 820ms var(--ease-in-out), transform 240ms var(--ease-pop); }
.ab-shot__ask > svg { transition: width 820ms var(--ease-in-out),
  height 820ms var(--ease-in-out); }
.ab-shot__phead, .ab-shot__chat { transition: opacity 520ms var(--ease-out-expo); }
.ab-shot__panel { transition: background-color 620ms var(--ease-out-expo),
  box-shadow 620ms var(--ease-out-expo); }

/* the press: the button dips and comes back, so the send is something that
   happened rather than something that simply stopped */
.ab-shot__send.is-sending { transform: scale(.82); }

/* One frame with every transition off, so the intro's opening layout is
   laid out rather than animated into. It has to reach descendants — the
   grid lives on .ab-shot but the composer's width lives on a child — and
   !important is the honest tool for a blanket one-frame override; matching
   each rule's specificity by hand would break the first time one changed. */
.ab-shot.no-anim, .ab-shot.no-anim * { transition: none !important; }
.ab-shot.solo:not(.compose) .ab-shot__panel {
  box-shadow: 0 2px 6px rgba(26,26,26,.08), 0 44px 90px -34px rgba(26,26,26,.36); }
html[data-theme="dark"] .ab-shot.solo:not(.compose) .ab-shot__panel {
  box-shadow: 0 2px 6px rgba(0,0,0,.5), 0 44px 90px -34px rgba(0,0,0,.72); }

.ab-shot.intro .ab-shot__panel { opacity: 0;
  transition: opacity 560ms var(--ease-out-expo), transform 820ms var(--ease-in-out); }
.ab-shot.intro.intro-panel .ab-shot__panel { opacity: 1; }
.ab-shot.intro .ab-shot__grid > * { opacity: 0; }
.ab-shot.intro.intro-main .ab-shot__grid > * {
  animation: ab-cardin 560ms var(--ease-out-expo) both; }
@keyframes ab-cardin {
  from { opacity: 0; transform: translateY(14px) scale(.97); }
  to   { opacity: 1; transform: none; }
}
/* the tile you press comes first, then the agent that was just asked for,
   then the rest of the fleet fills in behind them */
.ab-shot.intro.intro-main .ab-shot__grid > *:nth-child(1) { animation-delay: 0ms; }
.ab-shot.intro.intro-main .ab-shot__grid > *:nth-child(2) { animation-delay: 240ms; }
.ab-shot.intro.intro-main .ab-shot__grid > *:nth-child(3) { animation-delay: 470ms; }
.ab-shot.intro.intro-main .ab-shot__grid > *:nth-child(4) { animation-delay: 560ms; }
.ab-shot.intro.intro-main .ab-shot__grid > *:nth-child(5) { animation-delay: var(--dur-5); }
.ab-shot.intro.intro-main .ab-shot__grid > *:nth-child(6) { animation-delay: 710ms; }
.ab-shot.intro.intro-main .ab-shot__grid > *:nth-child(7) { animation-delay: 770ms; }
.ab-shot.intro.intro-main .ab-shot__grid > *:nth-child(8) { animation-delay: 820ms; }
.ab-shot.intro.intro-main .ab-shot__grid > *:nth-child(9) { animation-delay: 860ms; }

/* nothing leaves the hero until the intro is done */
body.is-intro .page { overflow: hidden; }

/* ---- tooltips, the way the app has them ----------------------------- */
.ab-tip { position: relative; }
.ab-tip::after { content: attr(data-tip); position: absolute; left: 50%; top: calc(100% + 7px);
  transform: translateX(-50%) translateY(-3px); white-space: nowrap;
  background: #2b2b2b; color: #f5f5f5; font-size: var(--text-2xs); letter-spacing: 0.01em;
  padding: 4px 8px; border-radius: 6px; opacity: 0; pointer-events: none;
  transition: opacity var(--dur-2) var(--ease-out-expo), transform var(--dur-2) var(--ease-out-expo); z-index: 5; }
.ab-tip:hover::after { opacity: 1; transform: translateX(-50%) translateY(0); }
.ab-shot__rail .ab-tip::after { left: calc(100% + 8px); top: 50%;
  transform: translateY(-50%) translateX(-3px); }
.ab-shot__rail .ab-tip:hover::after { transform: translateY(-50%) translateX(0); }

/* ---- the session list is an easter egg: closed until you ask for it -- */
.ab-shot__hist { display: none; }
.ab-shot__hist.is-open { display: flex; }
.ab-shot__hist span:not(.h) { cursor: pointer; }
.ab-shot__hist span:not(.h):hover { background: rgba(128,128,128,0.10); }
.ab-shot__histbtn { cursor: pointer; }
.ab-shot__histbtn.is-on { color: var(--s-accent); opacity: 1; }
/* the typing dots while NIX answers */
.ab-shot__typing { display: inline-flex; gap: 4px; padding: 9px 2px; }
.ab-shot__typing i { width: 5px; height: 5px; border-radius: 50%; background: currentColor;
  opacity: .35; animation: ab-type 1.2s ease-in-out infinite; }
.ab-shot__typing i:nth-child(2) { animation-delay: .15s; }
.ab-shot__typing i:nth-child(3) { animation-delay: .3s; }
@keyframes ab-type { 30% { opacity: .9; transform: translateY(-2px); } }

/* ---- Device: the reel ----------------------------------------------- *
 * Two rows drifting in opposite directions, full-bleed. Each pair is one
 * of the people the copy names: the wide panel is what that agent's
 * browser is looking at, the narrow one is the conversation with it. The
 * track is duplicated so the loop has no seam, and the whole thing pauses
 * when it is off screen.
 *
 * 100vw with no negative margin: the .ribbon on /services documents that the
 * margin trick double-shifts inside a centred flex column. */
.ab-reel { width: 100vw; margin-top: var(--space-8); overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 7%, #000 93%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 7%, #000 93%, transparent); }
/* One gutter for every seam. An earlier pass split it — tight between a
   screen and its agent panel, wide between pairs — by adding a margin to
   .ab-panel--chat on top of this gap; the pairing it drew was not the one
   the reel reads as, so the margin is gone and this is the only number
   that sets horizontal spacing. */
.ab-reel__row { display: flex; gap: 6px; width: max-content; }
/* Matches the row's own gap, so the band is one even grid on both axes. */
.ab-reel__row + .ab-reel__row { margin-top: 6px; }
/* These durations are per LOOP, not a speed, so they had to be re-timed
   after the panels grew: a repeat unit went from ~3390px to ~5315px, which
   silently made the same 64s/78s run 1.57x faster in pixels per second.
   128s/156s puts the drift at ~42 and ~34 px/s — slower than the reel has
   ever actually moved, and the 1.22 ratio between the two rows is kept so
   they still never line up. */
.ab-reel.is-live .ab-reel__row--a { animation: ab-reel-a 128s linear infinite; }
.ab-reel.is-live .ab-reel__row--b { animation: ab-reel-b 156s linear infinite; }
@keyframes ab-reel-a { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes ab-reel-b { from { transform: translateX(-50%); } to { transform: translateX(0); } }

.ab-panel { flex: 0 0 auto; border-radius: 1.17em; overflow: hidden; text-align: left;
  font-size: clamp(12px, 1.15vw, 17px);
  background: var(--nix-surface);
  box-shadow: 0 1px 2px rgba(26,26,26,.05), 0 18px 40px -18px rgba(26,26,26,.20); }
/* Height is set here, on both kinds at once, and NOT left to aspect-ratio.
   The row is a flex container with the default align-items:stretch, so
   every panel was already being stretched to the tallest one — the chat,
   at width x 4/3 — and .ab-panel--view's 16/10 never applied at all. That
   also meant the band's height was a side effect of the chat panel's
   WIDTH clamp, which is why the two kinds drifted ~30px apart at mid
   widths even though they look identical at full size. An explicit height
   makes them equal by construction and is the one number to turn. */
/* Sized off the Framer showcase band: tiles about 36vw tall with the
   landscape one about 53vw wide. Both widths move with the height on
   purpose — raising the height alone squares the browser window off and it
   stops reading as one. The three clamps hold the proportions the reel
   already had (view a step and a half) at roughly 1.7x the size. The chat is narrower
   than that ratio on purpose — it is a docked side panel, not a second
   window, and at under one it read as one. */
.ab-panel { height: clamp(320px, 34vw, 500px); }
.ab-panel--view { width: clamp(500px, 53vw, 780px); }
.ab-panel--chat { width: clamp(200px, 20vw, 296px); }

/* Everything inside a panel is in em off the line above, so the mock UI
   scales with the box instead of sitting in it. In px it did not: at the
   old ~280px the fixed 12px ramp read as a small screenshot, and at 500px
   it read as the same small screenshot with 300px of dead room under it.
   The em values are the old px over a 12px base, so full-size renders
   ~1.4x and nothing about the proportions changed. */
.ab-panel__chrome { display: flex; align-items: center; gap: 0.42em; padding: 0.67em 0.83em;
  background: rgba(128,128,128,0.12); }
.ab-panel__chrome i { width: 0.5em; height: 0.5em; border-radius: 50%; background: rgba(128,128,128,0.45); }
.ab-panel__chrome b { flex: 1; height: 0.67em; margin-left: 0.5em; border-radius: 0.33em; background: rgba(128,128,128,0.22); }
.ab-panel__body { padding: 1em; display: flex; flex-direction: column; gap: 0.58em; height: 100%; }
.ab-panel__row { display: flex; align-items: center; gap: 0.5em; }
.ab-panel__row i { height: 0.58em; border-radius: 0.33em; background: rgba(128,128,128,0.26); }
.ab-panel__row i.f { flex: 1; }
.ab-panel__row i.n { width: 2.83em; background: rgba(128,128,128,0.36); }
.ab-panel__row i.g { width: 2.83em; background: #4a9d6a; opacity: 0.7; }
.ab-panel__row i.a { width: 3.67em; background: var(--nix-ink); opacity: 0.35; }
.ab-panel__hero { flex: 1; min-height: 0; border-radius: 0.58em; background: rgba(128,128,128,0.20); }
.ab-panel__btn { height: 1.08em; width: 40%; border-radius: 0.58em; background: rgba(128,128,128,0.38); }
.ab-panel__gut { display: flex; gap: 0.67em; flex: 1; min-height: 0; }
.ab-panel__gut u { width: 0.83em; border-radius: 0.25em; background: rgba(128,128,128,0.18); text-decoration: none; }
.ab-panel__gut s { flex: 1; display: flex; flex-direction: column; gap: 0.5em; text-decoration: none; }

/* the narrow panel: a conversation with that agent */
.ab-panel--chat .ab-panel__body { gap: 0.67em; padding: 1em 0.92em; }
.ab-panel__who { font-size: 0.83em; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--nix-text-soft); }
.ab-panel__said { font-size: 1em; line-height: 1.45; border-radius: 0.92em; padding: 0.67em 0.83em;
  background: rgba(128,128,128,0.14); }
.ab-panel__said--back { background: transparent; padding: 0 0.17em; color: var(--nix-text-soft); }
.ab-panel__ask { margin-top: auto; height: 2.17em; border-radius: var(--radius-pill); background: rgba(128,128,128,0.10); }

/* ---- This is an agentic computer ------------------------------------ *
 * Copy on the left, a diagram on the right: any agent, one computer, and
 * what the computer can make.
 *
 * The line language is the preloader's, curved. the preloader draws its rule as
 * a faint --nix-rule track with a short --focus-ink segment scanning
 * along it; these are the same two things bent into cubics. --focus-ink
 * is the right token even though it currently resolves to black — tokens.css
 * says "accent runs black while the site is monochrome", so if that ever
 * becomes a colour this follows it instead of holding a hardcoded grey. */
.ab-what { width: min(1190px, 100%); margin: 0 auto; display: grid;
  grid-template-columns: 1fr 1fr; gap: var(--space-6); align-items: center; }
/* components.css sets BOTH properties on this element —
   .landing__block .landing__text { text-align: justify; text-align-last:
   center } — so overriding text-align alone leaves the last line centred
   under a left-aligned paragraph, which is what it did. text-align-last
   has to be named too. */
.ab-what__copy { text-align: left; }
.ab-what__copy .landing__headline,
.ab-what__copy .landing__text { text-align: left; text-align-last: auto;
  margin-left: 0; margin-right: 0; }

/* The hero's recipe, not the page's flat card. .ab-shot and .ab-panel both
   carry NO border — a surface and two shadows, one tight and one wide — and
   the app's own UI does the same, so a 1px rule around anything in here read
   as belonging to a different drawing. The frame's own tone is a shade under
   the chips that sit on it, which is what gives the shadows something to
   fall on.

   Radius 18 rather than 25: that is .ab-shot's own corner, and this is
   meant to be the same object seen from further away. */
/* The card takes --nix-surface, which is how the palette works in both
   directions: in light it is #FFFFFF, the same as the page, and the comment
   beside it in tokens.css says so — "cards and chips, lifted by shadow". In
   dark it is #201F1E against a #131312 page, a real step. The first pass set
   the card to #131313, which IS the dark page tone, so the frame was
   invisible.

   The chips then need one more step, and only in dark: on white they
   separate the way everything else on this page does, by shadow. */
/* The diagram card sits ON something instead of floating on the page: the
   hero's canvas under the hero's panel tone, #201F1E on #0e0e0e, which is
   the same lift the agents panel gets at the top of the page. It wraps the
   card only — the column beside it stays on the page, the way the hero's
   own copy does. */
.ab-what-frame { border-radius: 18px; padding: 7px;
  background: #ffffff;
  box-shadow: 0 2px 6px rgba(26,26,26,.08), 0 44px 90px -34px rgba(26,26,26,.36); }
html[data-theme="dark"] .ab-what-frame { background: #0e0e0e;
  box-shadow: 0 2px 6px rgba(0,0,0,.5), 0 44px 90px -34px rgba(0,0,0,.72); }

/* The card inside takes the panel radius that goes with a 7px frame — 18
   outside and 18 inside leaves a ring of frame that is thinner at the
   corners than along the sides. */
.ab-what-frame .ab-wire-card { border-radius: 13px; }

.ab-wire-card { --w-chip: var(--nix-surface);
  background: var(--nix-surface); border-radius: 18px; padding: var(--space-4);
  box-shadow: 0 2px 6px rgba(26,26,26,.08), 0 44px 90px -34px rgba(26,26,26,.36); }
/* One ladder for the whole page: #0e0e0e frame, #1a1a1a surface on it,
   #242424 for the chips on that. Same three tones as .ab-shot at the top
   and .ab-ui below, so the diagram is not the only card on the page with
   a surface of its own — --nix-surface's #201F1E sat between two of them
   and read as a fourth. */
/* In light the frame is the paper and the card is the screen recessed in
   it, which is the same three steps read the other way round: on white,
   a surface separates by going DOWN, and --nix-surface's white on a white
   frame was two sheets of paper with a shadow between them. */
.ab-what-frame .ab-wire-card { background: #eeeeee; }
html[data-theme="dark"] .ab-what-frame .ab-wire-card { background: #1a1a1a; }
html[data-theme="dark"] .ab-wire-card { --w-chip: #242424; }

.ab-wire { width: 100%; height: auto; display: block; overflow: visible; }
/* .ab-panel's shadow pair, transposed onto SVG: one tight shadow to seat
   the chip and one wide one to lift it. No stroke — that was the thing that
   did not belong.

   The alphas are the page's own (.05 and .20), not the .22/.30 this
   started on. Three chips a slot apart, each throwing a 30px blur at 30%
   onto a WHITE card, pooled into a solid grey slab — and the reel's mask
   cut that pool off at its window, which is what gave the slab its hard
   vertical edges. drop-shadow has no negative spread to pull it back in the
   way .ab-panel's box-shadow does, so the blur comes down instead.

   Dark keeps a heavier pair: there the shadow has to register against
   #201F1E rather than white, and there is a tone step doing half the work
   anyway. */
.ab-wire-card { --w-shadow: drop-shadow(0 1px 1px rgba(26,26,26,.05))
                            drop-shadow(0 8px 16px rgba(26,26,26,.11)); }
html[data-theme="dark"] .ab-wire-card {
  --w-shadow: drop-shadow(0 1px 2px rgba(0,0,0,.30))
              drop-shadow(0 10px 20px rgba(0,0,0,.38)); }

.ab-wire__chip { fill: var(--w-chip); stroke: none; filter: var(--w-shadow); }
.ab-wire__name, .ab-wire__lab { font-size: 13px; fill: var(--nix-text); }
.ab-wire__lab { font-size: 11.5px; }
/* The three marks are inlined rather than <image href>: an external file
   cannot inherit currentColor, so a referenced logo would stay its own
   colour and break on one of the two themes. Nested <svg> keeps each one's
   own viewBox, so the geometry is exact and only the box is scaled.

   Provenance, because a trademark should be traceable: Gemini and Claude
   from Simple Icons (CC0 files), ChatGPT from Wikimedia Commons
   (File:ChatGPT-Logo.svg, public domain). OpenAI is no longer in Simple
   Icons — removed on request — which is why it came from Commons instead.

   Commons has two candidates and only one of them can be inlined. The
   shorter file draws ONE lobe and repeats it with five rotated <use>
   elements, so lifting its path alone gives a sixth of a knot — which is
   exactly what it rendered as. This is the other one: a single complete
   path on a 320 box, self-contained.

   Nominative use: these name providers AB01 works with. */
.ab-wire__logo { color: var(--nix-text); overflow: visible; }
.ab-wire__bed { fill: none; stroke: var(--nix-rule); stroke-width: 1.5; }
/* The computer is the one filled thing, so it reads as the subject. */
.ab-wire__lid { fill: none; stroke: var(--nix-text); stroke-width: 3; }
.ab-wire__base { fill: none; stroke: var(--nix-text); stroke-width: 3;
  stroke-linecap: round; }

/* A 12-unit dash on a path normalised to 100 units, so one rule drives all
   six curves whatever their real length. */
.ab-wire__pulse { fill: none; stroke: var(--focus-ink); stroke-width: 2;
  stroke-linecap: round; stroke-dasharray: 12 100; stroke-dashoffset: 112;
  opacity: 0; }

/* The agents are a REEL, not three wires. Only the middle slot is
   connected — one wire, always in the same place — and the names ride
   past it: hold for 2.2s, step one slot in 0.6s, hold again. That is what
   "one at a time" looks like when the thing they connect to never moves.

   Eight cells for four names. The list is doubled so the step that lands
   on the last one can carry straight into a copy of the first: at
   -232 the window shows cells 5, 6, 7, which are copies of 1, 2, 3 — the
   same frame as translateY(0), so the loop has no seam to hide.

   The gradient's full-opacity band is 0.34 to 0.66 of the window, which is
   narrower than it looks like it should be: the outer slots' centres sit at
   20% and 80%, so stops at 0.2/0.8 left them at full strength and all three
   cells read equally lit. Pulled in, only the middle one is.

   maskUnits="userSpaceOnUse" is not decoration. A <mask> element defaults
   to objectBoundingBox for its REGION, which is 120% of the masked
   content's own box — the cells span 6 to 110, so the region ended around
   120 and every chip's shadow was sliced off in a straight vertical line a
   few units to its right. Widening the rect inside the mask did nothing,
   because the rect is content and the region is what clips. Named in user
   units, the region is the numbers written here.

   The rect inside it runs -40 to 160 although the cells only span 6 to 110,
   for the same reason: it has to clear the shadows rather than end on
   them.

   The window is masked with a gradient rather than clipped, so the cells
   above and below fade instead of being cut. A mask works on alpha, which
   means it does not need to know the card's colour — and the card's ground
   is translucent, so anything that did would be wrong. */
.ab-wire__reel { animation: ab-wire-reel 11.2s var(--ease-in-out) infinite; }
@keyframes ab-wire-reel {
  0%,   19.6% { transform: translateY(0); }
  25%,  44.6% { transform: translateY(-58px); }
  50%,  69.6% { transform: translateY(-116px); }
  75%,  94.6% { transform: translateY(-174px); }
  100%        { transform: translateY(-232px); }
}

/* The one incoming wire pulses on the hold, not during the step: a pulse
   arriving while the reel is moving would be coming from whichever name is
   halfway past. 2.8s to match one cell's turn. */
@keyframes ab-wire-in {
  0%    { stroke-dashoffset: 112; opacity: 0; }
  6%    { opacity: 1; }
  55%   { stroke-dashoffset: 0; opacity: 1; }
  70%   { opacity: 0; }
  100%  { stroke-dashoffset: 0; opacity: 0; }
}
.ab-wire__pulse--in { animation: ab-wire-in 2.8s linear infinite; }

/* THE THREE OUTPUT PULSES HAD NO KEYFRAMES AT ALL. Three rules named
   ab-wire-out, nothing defined it, and an animation-name that resolves to
   nothing is not an error in any engine — the declaration is simply dropped.
   So the paths kept their resting state, which is `opacity: 0`: the computer
   in this drawing has been taking work in and sending nothing out since the
   day the animation was written.

   Same shape as ab-wire-in and the reverse direction: the dash starts off the
   near end of the path and runs to the far one, fading in as it leaves and
   out as it arrives, so nothing pops at either end of the loop. */
@keyframes ab-wire-out {
  0%    { stroke-dashoffset: 112; opacity: 0; }
  8%    { opacity: 1; }
  70%   { stroke-dashoffset: 0; opacity: 1; }
  85%   { opacity: 0; }
  100%  { stroke-dashoffset: 0; opacity: 0; }
}

/* The outputs keep their own 2s loop: the computer goes on making things
   whichever name is currently wired to it. */
.ab-wire__pulse--out1 { animation: ab-wire-out 2s linear infinite 0.15s; }
.ab-wire__pulse--out2 { animation: ab-wire-out 2s linear infinite 0.45s; }
.ab-wire__pulse--out3 { animation: ab-wire-out 2s linear infinite 0.75s; }

/* Each tile is a square holding three faces — an icon and a name — stacked
   in the same place, one visible at a time. The turnover is a cross-fade
   rather than a swap: three faces on a 6.6s round, each up for 2.2s with
   half a second of overlap at both ends, so the tile is never empty and
   never shows two names at full strength.

   The stagger is per tile AND per face, which is why the delays are
   fractional: on a shared offset all three tiles turned over on the same
   frame and the whole column blinked at once.

   No opacity animation on .ab-wire__out itself any more. It used to lift
   the whole group, which would now fight the faces inside it for the same
   property. */
.ab-wire__face { opacity: 0; animation: ab-wire-face 6.6s linear infinite; }
@keyframes ab-wire-face {
  0%          { opacity: 0; }
  5%,  28%    { opacity: 1; }
  33%, 100%   { opacity: 0; }
}
.ab-wire__icon { fill: none; stroke: var(--nix-text); stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round; }
.ab-wire__lab { font-size: 11.5px; }

/* ---- The entrance ---------------------------------------------------- *
 * .ab-what keeps .reveal only to be TOLD when it is on screen — the
 * observer in this file adds .is-in to .reveal elements and nothing else,
 * so that class is the only hook available. Its own fade is cancelled
 * here: left on, the block would fade in as one object and then fade its
 * parts in again inside it, and the stagger would be happening underneath
 * a curtain that is still lifting.
 *
 * Everything below is opacity and a short lift, keyed off one class, with
 * the order the eye should read in: headline, sentence, then the card, and
 * inside the card the machine before the things wired to it. */
.ab-what.reveal { opacity: 1; transform: none; transition: none; }

.ab-what__copy .landing__headline,
.ab-what__copy .landing__text,
.ab-what-frame {
  opacity: 0; transform: translateY(12px);
  transition: opacity 760ms var(--ease-out-expo),
              transform 760ms var(--ease-out-expo); }
.ab-what.is-in .landing__headline,
.ab-what.is-in .landing__text,
.ab-what.is-in .ab-what-frame { opacity: 1; transform: none; }
.ab-what.is-in .landing__text { transition-delay: 110ms; }
.ab-what.is-in .ab-what-frame { transition-delay: 220ms; }

/* Inside the diagram: opacity only for anything that already has an
   animation on a transform, or the two would fight over the property. The
   machine has none, so it may lift. */
.ab-wire__box { opacity: 0; transform: translateY(8px);
  transition: opacity 620ms var(--ease-out-expo) var(--dur-4),
              transform 620ms var(--ease-out-expo) var(--dur-4); }
.ab-what.is-in .ab-wire__box { opacity: 1; transform: none; }

/* The wires draw themselves. Same idiom as the pulses that will run along
   them a moment later, and the reason pathLength="100" was worth having:
   one dash figure covers all four however long each curve really is. */
.ab-wire__bed { stroke-dasharray: 100; stroke-dashoffset: 100;
  transition: stroke-dashoffset var(--dur-6) var(--ease-out-expo) 520ms; }
.ab-what.is-in .ab-wire__bed { stroke-dashoffset: 0; }

.ab-wire__window, .ab-wire__out { opacity: 0;
  transition: opacity 620ms var(--ease-out-expo); }
.ab-what.is-in .ab-wire__window { opacity: 1; transition-delay: 700ms; }
.ab-what.is-in .ab-wire__out { opacity: 1; }
.ab-what.is-in .ab-wire__out--1 { transition-delay: 820ms; }
.ab-what.is-in .ab-wire__out--2 { transition-delay: var(--dur-6); }
.ab-what.is-in .ab-wire__out--3 { transition-delay: 980ms; }

/* The loops wait. Paused until the section is seen, so the reel is not
   four names into its round and a pulse is not halfway down a wire that
   has not been drawn yet — and nothing burns frames while it is offscreen.
   Negative delays still resolve while paused, so one face per tile and one
   name in the middle slot are already in place when it starts. */
.ab-wire__reel, .ab-wire__pulse, .ab-wire__face { animation-play-state: paused; }
.ab-what.is-in .ab-wire__reel,
.ab-what.is-in .ab-wire__pulse,
.ab-what.is-in .ab-wire__face { animation-play-state: running; }

/* ---- Easy to start -------------------------------------------------- *
 * ONE window, and the whole flow plays inside it as you scroll: three
 * steps, the machine being made, and the machine running. It used to be
 * three cards on a sliding track; a single window that changes is the same
 * story told the way the app tells it, and it drops the horizontal track,
 * its phone swipe fallback and the arithmetic that kept the two agreeing.
 *
 * The screens stack in normal flow until the script pins the section. That
 * is the base state, not a fallback bolted on: with no JS, no rAF or
 * reduced motion you get every screen, finished, one under the next.
 *
 * Nothing inside may be focusable. The browser scrolls a focused element
 * into view, and against a page-controlled layout that is a fight the
 * browser wins halfway through.
 *
 * 100vw with no negative margin, for the reason .ab-reel documents. */
.ab-start { width: 100vw; margin-top: var(--space-8);
  /* Same two numbers as the band, and for the same reasons: 30px clears
     the 28px fade the fixed header paints below itself, and the height is
     capped to the scrollport or a sticky box taller than its scrollport
     scrolls to its own bottom and unpins. */
  /* 30px clears the header's fade, and the heading parks in the band
     between that and the card — see the sticky rule below. */
  --start-lead: 58px;
  --start-top: calc(30px + var(--start-lead));
  --start-h: min(calc(100svh - var(--head-h) - var(--start-top) - var(--space-6)), 700px); }
.ab-start__pin { display: flex; flex-direction: column; align-items: center;
  padding: 0 var(--space-4); }
/* The frame the flow plays inside, on the hero's terms: 7px of canvas,
   18 outside and 13 in, and the canvas DARKER than what sits on it.
   It used to be the lighter of the two — a pale plate under a near-black
   window — which read as two cards stacked rather than a machine in a
   frame, and left the animated window as the darkest thing on the page.
   Inverting here is why .ab-ui's dark tones are lifted below: one of the
   two had to move, and the frame is the one that carries no content. */
/* overflow: hidden because the window inside is a flex child that can be a
   fraction of a pixel taller than the box it is in — enough for its own
   square-cut edge to cross the frame's rounded corner and read as a line
   that stops short of it. The frame owns its corners. */
.ab-start__inner { width: min(1190px, 100%); background: #ffffff;
  border-radius: 18px; padding: 7px; overflow: hidden;
  display: flex; flex-direction: column;
  box-shadow: 0 2px 6px rgba(26,26,26,.08), 0 44px 90px -34px rgba(26,26,26,.36); }
html[data-theme="dark"] .ab-start__inner { background: #0e0e0e;
  box-shadow: 0 2px 6px rgba(0,0,0,.5), 0 44px 90px -34px rgba(0,0,0,.72); }

/* The caption strip. A viewport with a row of captions behind it, moved by
   the same scroll that runs the flow: it holds while a screen's cursor
   works, then slides as that screen gives way. The blue pill in the mock
   was Figma's selection colour — this page has no chromatic accent
   anywhere, so it takes the same faint ground the card itself uses.
   It lives OUTSIDE the card: it describes the machine rather than being
   part of it, so it should not sit on the machine's own ground. */
/* overflow: hidden is the viewport the caption track slides behind, and it
   clips at the PADDING edge — so the pill's own shadow needs padding to
   live in, or it is cut flat top and bottom. The margins give back exactly
   what the padding added, leaving the gap to the card at --space-4. */
.ab-start__cap { width: min(1190px, 100%); overflow: hidden; flex: 0 0 auto;
  padding: 22px 0;
  margin-top: calc(var(--space-4) - 22px); margin-bottom: -22px; }
.ab-start__cap-track { display: flex; will-change: transform; }
.ab-start__cap-item { flex: 0 0 100%; display: flex; justify-content: center;
  padding: 0 var(--space-3); }
/* Two lines' worth, always, with the text centred in them.
   The pills are flex siblings on one track, so every one of them is already
   as tall as the tallest caption — which wraps to two lines on a phone. The
   text inside was top-aligned in that box, so a one-line caption sat under
   the pill's ceiling with a line of nothing beneath it. Centring fixes that
   half; the min-height fixes the other, where at a width that fits every
   caption on one line the pills would all shrink to one and the strip would
   change height as it slid. */
/* White in light, the same paper the frame above it is on, held by the
   site's shadow — a translucent grey on a white page is a smudge, and this
   line is the one thing on the page describing what the machine just did.
   Dark keeps the faint ground: white there would shout over the card. */
.ab-start__cap-item > span { background: #ffffff;
  box-shadow: var(--lift-1);
  border-radius: var(--radius-pill); padding: 13px 26px; font-size: 15px; line-height: 1.35;
  text-align: center; display: flex; align-items: center; justify-content: center;
  min-height: calc(2 * 1.35em + 26px); box-sizing: border-box; }
html[data-theme="dark"] .ab-start__cap-item > span {
  background: rgba(128,128,128,0.10); box-shadow: none; }

/* Unpinned there is no flow to caption, so the row becomes a list under the
   five stacked screens rather than a viewport showing one of five. */
.ab-start:not(.is-pinned) .ab-start__cap { overflow: visible; }
.ab-start:not(.is-pinned) .ab-start__cap-track { flex-direction: column;
  align-items: center; gap: 8px; }
.ab-start:not(.is-pinned) .ab-start__cap-item { flex: 0 0 auto; }

/* How much scroll the whole flow costs. At 400svh it was 0.8 of a
   scrollport per screen, so one flick of a wheel took a whole step and the
   thing felt twitchy — the demonstration went by faster than it could be
   read. 700svh was still fast. 1000svh gives each screen two full
   scrollports, so a step takes a deliberate scroll rather than a flick.
   One number, named, because it is the only knob worth turning here: the
   beats and the screen boundaries are all shares of it. */
.ab-start { --start-run: 1000svh; }
.ab-start.is-pinned { height: calc(var(--start-h) + var(--start-run)); }
/* No overflow here. The pin is exactly --start-h tall and the frame's wide
   shadow reaches past its own box, so clipping at the pin cut the shadow
   off in a straight line under the card. Nothing needs this clip: the
   caption viewport, the window and the frame each hide their own overflow,
   and overflow-x alone is not an option — it forces overflow-y to auto and
   turns a sticky box into a scroll container. */
.ab-start.is-pinned .ab-start__pin { position: sticky; top: var(--start-top);
  height: var(--start-h); }
/* The card takes what the caption leaves, rather than the full height of
   the pin — the strip is a sibling now, not a child. */
.ab-start.is-pinned .ab-start__inner { flex: 1; min-height: 0; }
/* "Easy to start." stays on screen for the whole flow. It is a sibling of
   the pin, not a child, so on its own it scrolled away the moment the card
   stuck — the reader watched five screens of a wizard with nothing saying
   what they were watching. Sticky at the same 30px the card used to take,
   and the card starts --start-lead lower so the two never meet. */
.landing__block--flow > .landing__headline { position: sticky; top: 30px; z-index: 2; }

/* ---- Agent Base, drawn ---------------------------------------------- *
 * The three cards in #start show the app's own New agent wizard instead
 * of the abstract bars .ab-panel draws. Same approach as .ab-shot in the
 * hero: the product rebuilt in markup rather than screenshotted, so it
 * scales to the card, picks up the page's font, and cannot go stale as a
 * 2000px PNG would.
 *
 * It follows the site's theme, the same way .ab-shot does and by the same
 * mechanism: a set of variables on the element, an html[data-theme="dark"]
 * override. There is no prefers-color-scheme fallback and there is no need
 * touched the toggle. AB01 itself only has the dark appearance, so the
 * light rendition is a stylisation — but .ab-shot has been drawing the
 * dashboard light in light mode since before this section existed, and one
 * mock that ignores the toggle while the other obeys it is worse than a
 * stylisation.
 *
 * One thing stays fixed at every theme, on purpose: the guest's browser is
 * a real web page inside the machine, not part of the app's chrome, so it
 * is light in both.
 *
 * Every interior length is em off one font-size, the way .ab-panel does
 * it, so the whole window scales with whatever box it lands in. */
/* position: relative because the cursor is absolutely placed inside it. */
.ab-ui { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column;
  /* Light follows .ab-shot's order: the canvas is the DARKEST tone and the
     surfaces on it are lighter. At a canvas of #f1f1f1 with #e9e9e9 boxes
     the two were a hair apart and the boxes disappeared. */
  --u-canvas: #eeeeee;   /* the window */
  --u-panel:  #ffffff;   /* the chat beside the machine */
  --u-page:   #fbfbfb;   /* the guest's browser */
  --u-box:    #f7f7f7;   /* the resource boxes */
  --u-field:  #e4e4e4;   /* inputs, selects */
  --u-press:  #d4d4d4;   /* one of those, held down */
  --u-menu:   #ffffff;
  --u-text:   #191919;
  --u-soft:   #4f4f4f;
  --u-dim:    #7a7a7a;
  --u-solid:  #191919;   /* filled button, live dot, the ring */
  --u-on:     #ffffff;   /* what sits on --u-solid */
  --u-solidp: #3a3a3a;
  --u-veil:   rgba(0,0,0,0.07);
  background: var(--u-canvas); color: var(--u-text);
  border-radius: 13px; overflow: hidden;
  font-size: clamp(10px, 0.92vw, 15px); text-align: left; }
html[data-theme="dark"] .ab-ui {
  --u-canvas: #1a1a1a; --u-panel: #242424; --u-box: #202020;
  /* The guest's page is a real web page and stays light in both themes. */
  --u-page: #ededed;
  --u-field: #2e2e2e; --u-press: #3b3b3b; --u-menu: #282828;
  --u-text: #f2f2f2; --u-soft: #b9b9b9; --u-dim: #8c8c8c;
  --u-solid: #f2f2f2; --u-on: #131313; --u-solidp: #cfcfcf;
  --u-veil: rgba(255,255,255,0.08); }

/* Absolute, so the title is centred on the WINDOW rather than on the space
   the lights and the sparkle leave over — which is what the app does and
   what a flex row would quietly get wrong by the width of the chevron. */

.ab-ui__stage { flex: 1; min-height: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 1.1em;
  padding: 0.5em 1.6em 1.6em; }
.ab-ui__ask { font-size: 2.4em; font-weight: 300; letter-spacing: -0.02em;
  line-height: 1.1; margin: 0 0 0.5em; text-align: center; }

/* One recipe for the text field and the two dropdowns: in the app they are
   the same control with different contents. */
/* min-height, not just padding. The key field is emptied and refilled by
   the cursor, and an empty one has no line box: it collapsed to a sliver
   mid-fill and sprang back, which read as the layout breaking rather than
   as typing. */
/* The select is the menu's containing block, so the list hangs off the
   control rather than off the window. */
.ab-ui__sel { position: relative; }
/* Opened by the cursor, on the beat that presses it, and closed again when
   the cursor leaves for the next control. UNVERIFIED, and the one thing
   here that is: the provider and model lists were not checked against the
   app. The memory, core and disk sizes are ordinary values, but which
   providers AB01 offers is a claim — do not publish these three without
   reading them off the real screen. */
.ab-ui__menu { position: absolute; left: 0; right: 0; top: calc(100% + 0.45em);
  z-index: 4; padding: 0.3em; border-radius: 0.7em; background: var(--u-menu);
  display: flex; flex-direction: column; text-align: left;
  box-shadow: 0 18px 40px -14px rgba(0,0,0,.45);
  opacity: 0; transform: translateY(-0.3em); pointer-events: none;
  transition: opacity 200ms var(--ease-out-expo), transform 200ms var(--ease-out-expo); }
/* The OPEN select is what rises, not just its menu. The select is
   positioned but has z-index auto, so a z-index on the menu alone is
   resolved against the nearest stacking context and still loses to the
   positioned controls that come after it in the markup — the list drew
   underneath the key field and the model picker. */
.ab-ui__sel.is-open { z-index: 5; }
.ab-ui__sel.is-open .ab-ui__menu { opacity: 1; transform: none; }
.ab-ui__opt { padding: 0.5em 0.7em; border-radius: 0.45em; font-size: 0.92em;
  color: var(--u-soft); }
.ab-ui__opt.on { background: var(--u-veil); color: var(--u-text); }

.ab-ui__field, .ab-ui__sel { width: min(24em, 82%); box-sizing: border-box;
  background: var(--u-field); border-radius: 0.7em; padding: 0.85em 1.1em;
  min-height: 3em; display: flex; align-items: center; font-size: 1.05em; }
.ab-ui__field { justify-content: center; color: var(--u-dim); }
/* The caret the screen is showing mid-type. */
.ab-ui__caret { display: inline-block; width: 1px; height: 1.15em;
  background: var(--u-text); margin-left: 1px; vertical-align: text-bottom; }
.ab-ui__sel svg { width: 1.1em; height: 1.1em; margin-left: auto; opacity: 0.6; }
.ab-ui__mask { letter-spacing: 0.06em; color: var(--u-text); }

.ab-ui__btn { background: var(--u-solid); color: var(--u-on); border-radius: 0.7em;
  padding: 0.7em 1.5em; font-size: 1.05em; margin-top: 0.6em; }

.ab-ui__dots { display: flex; align-items: center; gap: 0.6em; margin-top: 0.9em; }
.ab-ui__dots i { width: 0.45em; height: 0.45em; border-radius: var(--radius-pill); background: var(--u-dim); opacity: 0.6; }
.ab-ui__dots i.on { width: 1.6em; background: var(--u-solid); opacity: 1; }

/* Resources: two boxes side by side, each a label, the control, and the two
   lines of arithmetic the app shows under it. */
.ab-ui__pair { display: flex; gap: 0.8em; width: min(46em, 100%); }
.ab-ui__box { flex: 1; background: var(--u-box); border-radius: 0.85em;
  padding: 1.1em 1em; display: flex; flex-direction: column;
  align-items: center; gap: 0.7em; text-align: center; }
.ab-ui__lab { font-size: 0.9em; color: var(--u-dim); }
.ab-ui__box .ab-ui__sel { width: 100%; justify-content: center; position: relative; }
.ab-ui__box .ab-ui__sel svg { position: absolute; right: 0.9em; }

/* Screens. In flow until the section is pinned, which is what makes the
   unscripted page readable: five finished screens, one under the next.
   Pinned, they are stacked in the same box and only the live one is up.

   The crossfade is a class and a CSS transition rather than another thing
   scrubbed by scroll position. Scrubbing an opacity across a scroll wheel
   reads as a smear; a fixed 380ms settles wherever you stop, and it plays
   in both directions on its own. */
.ab-ui__screens { position: relative; flex: 1; min-height: 0; display: flex;
  flex-direction: column; }
.ab-ui__screen { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.ab-start.is-pinned .ab-ui__screen { position: absolute; inset: 0; opacity: 0;
  transform: translateY(0.6em); pointer-events: none;
  transition: opacity var(--dur-4) var(--ease-out-expo), transform var(--dur-4) var(--ease-out-expo); }
.ab-start.is-pinned .ab-ui__screen.is-on { opacity: 1; transform: none; }
/* In flow they need a line between them, or five screens read as one very
   tall screen. */
.ab-start:not(.is-pinned) .ab-ui__screen + .ab-ui__screen {
  border-top: 1px solid var(--u-veil); }

/* Making the machine. UNVERIFIED — there is no screenshot of this step and
   it was not checked against the app; the wording is deliberately generic
   and the three lines below describe what the FAQ already says happens (an
   overlay on the base image, then a boot). Do not treat it as a portrait
   of a real screen the way the three wizard steps are. */
/* Powering on. The ring fills with the scroll the way the bar it replaced
   did, but it is the shape the app uses while a machine boots. */
.ab-ui__ring { width: 3.4em; height: 3.4em; }
.ab-ui__ring svg { width: 100%; height: 100%; display: block; transform: rotate(-90deg); }
.ab-ui__ring circle { fill: none; stroke-width: 2.4; }
.ab-ui__ring .t { stroke: var(--u-veil); }
.ab-ui__ring .p { stroke: var(--u-solid); stroke-linecap: round;
  stroke-dasharray: 125.66; stroke-dashoffset: 125.66; }
.ab-ui__boot { margin: 0.9em 0 0; font-size: 1.15em; color: var(--u-soft); }
.ab-ui__bootsep { color: var(--u-dim); }

/* The machine, running: its screen, and the panel you talk to it in.
   UNVERIFIED for the same reason as the step above. */
/* The hero's frame, with the machine in it instead of the dashboard. The
   numbers are .ab-shot's own — 7px of inset, 7px between, 13px corners —
   and the panel takes --s-panel's dark. An earlier pass gave the chat no
   ground at all, reading the first screenshot as though it shared the
   window's surface; it does not, it is a panel like the other one. */
/* One centred thing now, not two columns. The pane and the panel are
   still in the markup behind t-if="False", so the grid rule and its
   stacked phone variant below are left where they are for whenever they
   come back. */
.ab-ui__mach { flex: 1; min-height: 0; display: flex;
  align-items: center; justify-content: center; gap: clamp(1em, 4%, 3.5em);
  padding: 7px;
  /* The page's own ground, not the window's. On this screen the machine
     has stopped being a window with panes in it, so the darker canvas
     underneath it only draws a box around something that is no longer in
     a box. Same paper, same wash across it. */
  background: radial-gradient(90% 70% at 70% 42%, rgba(255, 255, 255, 0.055), transparent 70%),
              var(--nix-paper); }
/* The guest's browser is LIGHT: it is a real page inside the machine, not
   part of the app's chrome, and painting it dark would have been the one
   invented detail that contradicts the screenshot. */
.ab-ui__pane { background: var(--u-page); color: #1a1a1a; border-radius: 13px;
  overflow: hidden; display: flex; flex-direction: column; }
.ab-ui__side { background: var(--u-panel); border-radius: 13px;
  overflow: hidden; display: flex; flex-direction: column; }
/* Just the orb now — the three window dots are gone from the browser
   pane. justify-content rather than the orb's own margin-left: auto, which
   needed something on the left to push away from. */
.ab-ui__panebar { display: flex; align-items: center; justify-content: flex-end;
  padding: 0.75em 0.9em; }
.ab-ui__orb { width: 1.9em; height: 1.9em; border-radius: 50%;
  background: #141414; color: #fff; display: flex; align-items: center;
  justify-content: center; }
.ab-ui__orb svg { width: 1em; height: 1em; display: block; }
.ab-ui__web { padding: 1.6em 2em; display: flex; flex-direction: column; gap: 0.9em; }
.ab-ui__webh { font-size: 1.5em; }
.ab-ui__webp { font-size: 0.95em; line-height: 1.5; max-width: 34em; color: #2a2a2a; }
.ab-ui__weba { font-size: 0.95em; color: #3b5bd0; text-decoration: underline; }

.ab-ui__sidebar { display: flex; align-items: center; gap: 0.6em; padding: 0.9em; }
.ab-ui__sideic { opacity: 0.65; }
.ab-ui__sideic svg { width: 1.15em; height: 1.15em; display: block; }
.ab-ui__sidename { flex: 1; display: flex; align-items: center; justify-content: center;
  gap: 0.35em; font-size: 1.05em; }
.ab-ui__sidename svg { width: 0.9em; height: 0.9em; opacity: 0.6; }
.ab-ui__sidebody { flex: 1; min-height: 0; }
/* The last screen is the guide's own composition, in the machine's frame:
   what it just became on the left, the drop on the right. Static text and
   not buttons — this is a picture of an interface, and a mock whose chips
   could be pressed would be lying about what it is.

   The old note below still holds for the drop itself. */
/* Sized to its text rather than to the room. flex:1 pushed the column
   hard left and the drop hard right, and the pair read as two things on
   the same screen instead of one arrangement. */
.ab-ui__guide { flex: 0 1 auto; max-width: 26em; display: flex;
  flex-direction: column; align-items: flex-start; gap: 0.5em; }
.ab-ui__gsaid { color: var(--u-dim); font-size: 0.95em; }
.ab-ui__gask { font-size: 1.9em; letter-spacing: -0.015em; line-height: 1.15;
  margin: 0.25em 0 0.9em; display: flex; align-items: baseline; gap: 0.12em; }
.ab-ui__gchip { padding: 0.5em 0.95em; border-radius: var(--radius-pill);
  background: var(--u-veil); font-size: 0.95em; }

/* The last screen is the drop and nothing else. The browser pane and the
   chat panel are still in the markup behind t-if="False" — this is the
   payoff of the whole run, and what it should say at that moment is the
   thing itself, not a window with an example page in it.

   Square, because the scene sizes itself off this box and a drop in a
   rectangle is an ellipse. */
.ab-ui__face { flex: 0 0 auto; width: min(34%, 40svh); aspect-ratio: 1; }
.ab-ui__facec { display: block; width: 100%; height: 100%; }
.ab-ui__composer { display: flex; align-items: center; gap: 0.6em; margin: 0.9em;
  padding: 0.55em 0.55em 0.55em 0.9em; border-radius: var(--radius-pill); background: var(--u-veil); }
.ab-ui__plus { opacity: 0.6; font-size: 1.1em; line-height: 1; }
.ab-ui__ph { flex: 1; color: var(--u-dim); font-size: 0.95em; }
.ab-ui__send { width: 1.9em; height: 1.9em; border-radius: 50%; background: var(--u-solid);
  color: var(--u-on); display: flex; align-items: center; justify-content: center; }
.ab-ui__send svg { width: 1em; height: 1em; display: block; }
.ab-ui__side { padding: 0.9em; gap: 0.6em; }

/* ---- The cursor, and the controls it works ---------------------------
   The section is already scroll-driven, so the demonstration is too: the
   pointer's position and every control's state are pure functions of how
   far down the page you are, recomputed each frame. Nothing runs on a
   timer, which is what lets scrolling back up play it backwards instead
   of leaving three finished screens behind you.

   All of it is additive over markup that already shows the screens
   FINISHED — the name typed, the key filled. With no script, reduced
   motion, or the swipe fallback, that is what you get: three completed
   screens and no cursor. A correct resting state, not a broken one. */
.ab-ui__ptr { position: absolute; left: 0; top: 0; width: 2.5em; height: 2.5em;
  opacity: 0; pointer-events: none; z-index: 6;
  filter: drop-shadow(0 2px 5px rgba(0,0,0,.4)); }
.ab-ui__ptr svg { width: 100%; height: 100%; display: block; }
.ab-ui.is-acting .ab-ui__ptr { opacity: 1; }

/* The press is a scale, not a colour swap: two of the three kinds of
   target are already the brightest thing on the screen and have nowhere
   brighter to go. */
.ab-ui__field, .ab-ui__sel, .ab-ui__btn {
  transition: transform 140ms var(--ease-out-expo),
              background-color 140ms var(--ease-out-expo); }
.ab-ui [data-beat].is-press { transform: scale(0.97); }
.ab-ui__field[data-beat].is-press,
.ab-ui__sel[data-beat].is-press { background: var(--u-press); }
.ab-ui__btn[data-beat].is-press { background: var(--u-solidp); }

/* The caret belongs to the field only while it is being typed into. */
.ab-ui__caret { opacity: 0; }
.ab-ui__field.is-typing .ab-ui__caret { opacity: 1; animation: ab-blink 1.05s step-end infinite; }

/* ---- Band ---------------------------------------------------------- *
 * Three sections that share one agent panel. The panel is NOT inside a
 * section — it is a sibling of all three, stuck to the viewport while the
 * left column scrolls past under it, which is the whole point: one agent,
 * several screens.
 *
 * That constraint is what fixes the DOM. position:sticky is clipped by the
 * element's own parent, so a panel living inside section 1 would come
 * unstuck the moment section 1 ended. It has to be a child of the box that
 * spans all three.
 *
 * 100vw with no negative margin, for the reason .ab-reel documents. */
.ab-band { width: 100vw; margin-top: var(--space-8);
  padding: 0 var(--space-4); display: grid;
  gap: var(--space-8) 12px;
  grid-template-columns: 1fr clamp(240px, 20vw, 320px);
  align-items: start;
  /* ONE height for the card and the panel. They used to come from
     different places — the card from the canvas aspect-ratio, the panel
     from the viewport — so they matched only at whichever window height
     made the two formulas agree, and on a taller screen the panel ran past
     the card. The min() keeps it inside the scrollport, which is also what
     the sticky panel needs to stay pinned. */
  /* The fixed header paints a 28px fade below itself
     (body.is-flow .site-head::after). The panel used to pin at the top of
     the scrollport, which is inside that band, so the fade washed over its
     first rows. 30px clears it. */
  --band-top: 30px;
  --band-h: min(calc(100svh - var(--head-h) - var(--band-top) - var(--space-6)), 700px); }
/* No height here and none on the sections either: the canvas sets its own
   through aspect-ratio, and everything above it follows. An earlier pass
   pinned the band to 92svh, which is what made one section fill the screen
   exactly — wrong once there are three of them, because it turns scrolling
   the group into three separate full-screen stops. */
.ab-band__flow { grid-column: 1; grid-row: 2; display: flex; flex-direction: column; gap: var(--space-8); }
.ab-band__sec { display: block; }

/* The head belongs to the whole features block, not to a section, so there
   is exactly one and it spans both columns. Centred, and text only — it
   reads as the title of everything below rather than as the first card's
   own label. */
.ab-band__head { grid-column: 1 / -1; grid-row: 1; margin: 0; text-align: center; }
.ab-band__title { font-size: clamp(26px, 3.1vw, 44px); font-weight: 400;
  line-height: 1.1; letter-spacing: -0.03em; margin: 0 auto; max-width: 22ch; }

/* The card is the grey ground the canvas sits on, and the caption is inside
   it rather than under it — that is what makes the two read as one object
   with a label instead of an image and a stray line of text. */
/* Measured off the reference at 1440: card radius 25 over a 10px pad, canvas
   radius 15 inside it, 20px between the two columns, 20px of foot padding.
   The ground is faint on purpose — in the reference the canvas is LIGHTER
   than the card it sits in, and at the 0.10 this started on the two tones
   were close enough to read as one muddy block. */
.ab-band__card { background: rgba(128,128,128,0.055); border-radius: var(--radius-2xl);
  height: var(--band-h);
  padding: 10px; display: flex; flex-direction: column; gap: 5px; }
/* Just the note now, on the card's bottom edge. */
.ab-band__foot { text-align: left; padding: 20px; }
.ab-band__note h4 { font-size: clamp(16px, 1.25vw, 18px); font-weight: 400;
  line-height: 1.35; letter-spacing: -0.01em; margin: 0 0 6px; }
.ab-band__note p { color: var(--nix-text-soft); line-height: 1.5; margin: 0; max-width: 54ch; }

/* Sticky, and three things have to be true at once or it quietly misbehaves.
   align-self must not be stretch, because a stretched grid item fills its
   area and has no room to move inside it. The height has to leave the panel
   shorter than the scrollport, or it scrolls to its own bottom and unpins.
   And top is 0 on purpose: components.css gives .page--flow a padding-top of
   var(--head-h), so the scrollport's content edge already sits below the
   fixed header — adding the header height here counts it twice and parked
   the panel 82px lower than intended. */
.ab-band__chat { grid-column: 2; grid-row: 2; position: sticky; align-self: start; top: var(--band-top);
  height: var(--band-h); }

/* The panel is the one thing that stays put across all four cards, so a hard
   text change reads as a glitch in something meant to be continuous. */
.ab-band__chat .ab-panel__who,
.ab-band__chat .ab-panel__said { transition: opacity 200ms var(--ease-out-expo); }
.ab-band__chat.is-swapping .ab-panel__who,
.ab-band__chat.is-swapping .ab-panel__said { opacity: 0; }

/* Size only — every interior rule comes from .ab-panel unchanged, which is
   the point: this section is meant to BE the reel's pair, and a second set
   of mock classes would drift from the first one the next time either is
   touched. The type ramp is the one thing that cannot carry over: the reel
   sizes it for a 763px panel and the canvas here is wider than that. */
.ab-panel--band-view,
.ab-panel--band-chat { width: auto; height: auto; font-size: clamp(14px, 1.5vw, 22px);
  display: flex; flex-direction: column; }
/* The canvas takes its height from its width, not from the viewport. This
   works here and did NOT work in the reel: there the panels are flex items
   under the default align-items:stretch, which overrides an aspect-ratio
   height. Inside the card the canvas is a plain block, so the ratio holds. */
/* 15px flat, not the em-scaled radius .ab-panel carries for the reel: the
   card around it is a fixed 25px, and a radius that drifts with the type
   ramp stops being concentric with it.
   The canvas takes what the foot leaves, rather than setting the height
   itself through an aspect-ratio. That is what lets one variable drive both
   columns: with the ratio in charge the card's height depended on the
   foot's text length too, which no calc() on the panel could have matched. */
.ab-panel--band-view { flex: 1; min-height: 0; border-radius: 15px; }
.ab-panel--band-view .ab-panel__body { padding: 1em 2.2em; }
.ab-panel--band-chat { height: 100%; border-radius: var(--radius-2xl); }
/* .ab-panel__body is height:100% under a chrome bar inside an
   overflow:hidden box, so it runs past the panel by the height of that bar
   and the last row is clipped. The reel hides it — its panels are small and
   drift past. Here the panel is column flex and the body just takes the
   remainder. */
.ab-panel--band-view .ab-panel__body,
.ab-panel--band-chat .ab-panel__body { height: auto; flex: 1; min-height: 0; }
/* No .ab-panel__hero in the canvas markup, and none of its filling either:
   a flex:1 spacer is what left the reel's taller panels looking half-empty.
   The rows have to actually be there. */
.ab-panel--band-view .ab-panel__body { gap: 0.72em; }
/* Deliberately more rows than fit. .ab-panel already clips at overflow
   hidden, so the surplus reads as a document that continues past the
   window — which is what a screen full of work looks like. Counting rows
   to land exactly on the bottom edge cannot work anyway: the panel's
   height is a share of its width and the rows are a share of the type
   ramp, and the two do not move together. */
.ab-band__split { display: flex; gap: 1.5em; flex: 1; min-height: 0; overflow: hidden; }
.ab-band__main { flex: 1; display: flex; flex-direction: column; gap: 0.72em; }
.ab-band__rail { width: 22%; display: flex; flex-direction: column; gap: 0.72em; }
.ab-band__rail i { height: 0.58em; border-radius: 0.33em; background: rgba(128,128,128,0.20); }

/* ---- Bento --------------------------------------------------------- *
 * Nine cells on four columns: three wide (span 2) and six square. The wide
 * ones are cells 1, 5 and 9, which is what draws the diagonal — and it is
 * drawn by ORDER, not by placement. Auto-flow fills 2+1+1, 1+2+1, 1+1+2 by
 * itself, so nothing carries a grid-column start and reordering the markup
 * cannot leave a hole behind.
 *
 * Only the squares carry a ratio. A wide cell is 2W + gap across, not 2W,
 * so aspect-ratio 2/1 makes it W + gap/2 tall — six pixels more than the
 * squares beside it, which measured as two row heights in the same grid.
 * Every row holds at least one square, so the square sets the row and the
 * wide cell stretches into it under the default align-items: stretch. */
/* Full-bleed at 100vw with no negative margin, the way .ab-reel and
   .ab-band already do it. It used to be min(1190px, 100%), which on this
   page resolved to the content column — narrower than the two sections
   either side of it, so the grid read as the small thing between them.
   Squares are slightly taller than wide: at a plain 1 the cells came out
   shorter than the copy needed once they were this wide. */
/* margin-top only. The side margins used to be auto, which was right at
   min(1190px, 100%) and wrong the moment this went full-bleed: an auto
   margin absorbs FREE space, there is none to absorb when the item is
   wider than the line, so both resolve to 0 and the grid anchors to the
   content column's left edge instead of centring — 1440 wide starting at
   x=180, hanging 180px off the right and making the whole page scroll
   sideways. Without them the flex column centres it, the same way it
   centres .ab-band. */
.ab-bento { width: 100vw; margin: var(--space-8) 0 0;
  padding: 0 var(--space-4);
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; text-align: left; }
/* Copy on the bottom edge, the way the band's foot sits on its card. Top
   aligned, a square cell with two lines in it is one line of text and a
   hand's width of nothing under it. */
.ab-bento__cell { background: rgba(128,128,128,0.055); border-radius: var(--radius-2xl);
  padding: 24px; display: flex; flex-direction: column; justify-content: flex-end;
  gap: 8px; aspect-ratio: 1 / 1.12; }
.ab-bento__cell--wide { grid-column: span 2; aspect-ratio: auto; }
.ab-bento__title { font-size: clamp(15px, 1.15vw, 17px); font-weight: 400;
  line-height: 1.35; letter-spacing: -0.01em; margin: 0; }
.ab-bento__text { font-size: 13px; color: var(--nix-text-soft); line-height: 1.5;
  margin: 0; }

/* ---- Prices --------------------------------------------------------- *
 * Three cards straight under the bento, and deliberately NOT full-bleed
 * the way .ab-bento is: a price is read line by line, and at 100vw the
 * middle card runs ninety characters wide. Content column, same fill and
 * same 25px radius as a bento cell, so the two grids read as one family
 * doing two different jobs.
 *
 * No aspect-ratio anywhere here — the three lists are different lengths,
 * and a ratio would give three different heights. The grid's default
 * align-items: stretch already levels them. */
.ab-price { width: min(1000px, 100%); margin: var(--space-6) auto 0;
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; text-align: left; }
/* --nix-surface, not a 5.5% grey. That literal was barely a step against the page
   in both themes, so two of the three cards were text with no box — and this
   is the copy on the PUBLIC pricing section; the one in portal.css was fixed
   a commit earlier and this one was not. */
.ab-price__card { background: var(--nix-surface); border-radius: var(--radius-2xl);
  padding: 26px 24px; display: flex; flex-direction: column; gap: 13px; }
/* The middle card is the one meant to be taken, so it is the only ink
   surface on the page's own ground. --nix-ink flips with the theme, so it
   stays the dark card in light mode and the light one in dark — the
   highlight is the inversion, not a colour the site does not have. */
.ab-price__card--lead { background: var(--nix-ink); color: var(--nix-on-ink); }
.ab-price__name { font-size: clamp(15px, 1.15vw, 17px); font-weight: 400;
  line-height: 1.35; letter-spacing: -0.01em; margin: 0; }
/* Baseline row: the figure and its unit sit on one line, so "$10" and
   "USD, per month" never drift apart at any font size. */
.ab-price__fig { display: flex; align-items: baseline; gap: 8px; margin: 0;
  flex-wrap: wrap; }
.ab-price__amount { font-size: 34px; font-weight: 300; line-height: 1;
  letter-spacing: -0.02em; }
.ab-price__per { font-size: 12px; letter-spacing: 0.02em; color: var(--nix-text-soft); }
.ab-price__text { font-size: 13px; color: var(--nix-text-soft); line-height: 1.5; margin: 0; }
.ab-price__list { list-style: none; margin: 0; padding: 0; display: flex;
  flex-direction: column; gap: 9px; }
/* Same 6px hollow dot .ab-future__row uses, drawn from the list item so the
   markup stays a plain <li>. */
.ab-price__item { position: relative; padding-left: 18px; font-size: 13px;
  line-height: 1.5; color: var(--nix-text-soft); }
.ab-price__item::before { content: ''; position: absolute; left: 0; top: 0.52em;
  width: 6px; height: 6px; border-radius: 50%; border: 1px solid currentColor; }
/* One opacity for every soft thing on the ink card: --nix-text-soft is a
   paper-ground grey and goes muddy on ink. */
.ab-price__card--lead .ab-price__per,
.ab-price__card--lead .ab-price__text,
.ab-price__card--lead .ab-price__item { color: var(--nix-on-ink-soft); }
/* margin-top: auto pins the button to the floor, so three lists of
   different lengths still end on one line of buttons. */
.ab-price__cta { margin-top: auto; align-self: flex-start; padding: 12px 28px; }

@keyframes ab-cursor {
  0%, 100% { transform: translate(0, 0); }
  30%      { transform: translate(96px, 26px); }
  55%      { transform: translate(48px, 62px); opacity: 0.85; }
  80%      { transform: translate(124px, 8px); }
}
@keyframes ab-blink { 50% { opacity: 0; } }

/* ---- Device: roadmap ----------------------------------------------- *
 * Set apart, but without a dashed border: there is not one anywhere in
 * the shared sheets, and a dash is the most conventional "placeholder" cue there is. */
.ab-dev--future { background: transparent; box-shadow: none; border: 1px solid var(--nix-rule); }
.ab-future { display: flex; flex-direction: column; gap: 11px; }
.ab-future__row { display: flex; gap: 13px; align-items: baseline; font-size: 15px; font-weight: 300; line-height: 1.5; }
.ab-future__dot { width: 6px; height: 6px; border-radius: 50%; border: 1px solid var(--nix-text-soft); flex: 0 0 auto; transform: translateY(-2px); }

/* Landscape phones: a centred hero overflows at BOTH ends, and the fixed
   header sits over the top of it, so the wordmark slid under the bar. */
@media (max-height: 560px) {
  .landing__hero { justify-content: flex-start; min-height: 0; padding: var(--space-4) 0; }
  .ab-shotwrap { display: none; }
  /* No shot here means no overrun to clear, and 10svh of a 560px-tall
     viewport is dead screen on the format that has the least of it. */
  #jobs { margin-top: var(--space-4); }
}
/* Hidden by request (2026-08-04), NOT removed — flip this rule to bring
   them back: Easy to use (#band), What a whole computer means (#bento),
   What it costs (#pricing), Questions (#faq). */
#band, #bento, #pricing, #faq { display: none !important; }

/* ── Overrides that have to stand last ────────────────────────────────────
   A media query adds no specificity, so a block written above a rule of the
   same selector loses to it at every width. These were higher up and every
   declaration in them was dead — including the reduced-motion opt-out, which
   is not a nicety: it is what a reader who asked their operating system for
   no motion gets. Position is the only thing that makes them apply. */

@media (max-width: 900px) {
  /* One track. The NIX panel is hidden here and only the intro brings it
     back, absolutely positioned, so the agents panel is the whole grid —
     it used to share the row with the 48px rail. */
  .ab-shot { grid-template-columns: 1fr; }
  .ab-shot__panel { display: none; }
}

@media (max-width: 720px) {
  /* Taller than the desktop's min(600px, 60svh). One column means one card
     per row, and at 60svh the frame showed a card and a half — the second
     one cut through its own middle, which reads as a clipping bug rather
     than as a list that continues. 68svh gets two whole cards and the top
     of a third, which is what "there are more" should look like. */
  .ab-shot { grid-template-columns: 1fr;
    height: min(820px, 78svh);
    /* The same gutter every other section uses at this width. The mock was
       96vw — 8px a side — against --space-3's 24 on .ab-band, .ab-bento and
       #start's own card, so the hero ran wider than everything under it and
       the page had two different edges. */
    width: calc(100vw - 2 * var(--space-3)); }
  /* A COLUMN, not a one-column grid — and that is a bug fix, not a
     preference. Everything inside a card is absolutely positioned, so the
     card's content is zero tall; as a stretched grid item its aspect-ratio
     never got to set the row, and the rows came out 31px while the cards
     stayed 220. Eight cards, 31px apart, overlapping by 189 each: the pile
     that made the frame look broken. Measured, not guessed — the tops read
     857, 889, 920, 951 …

     This file already documents the same failure for .ab-panel in the reel
     ("the row is a flex container with the default align-items:stretch, so
     .ab-panel--view's 16/10 never applied at all"). In a flex COLUMN with
     flex: 0 0 auto the card's width is definite and the ratio gives it a
     real height, with no row in between to lose the argument to.

     3/2 rather than 16/10: a phone card is the full width of the frame, so
     at the desktop's ratio it is a very wide, very short letterbox. */
  .ab-shot__grid { display: flex; flex-direction: column; }
  .ab-shot__card, .ab-shot__new { flex: 0 0 auto; width: 100%; aspect-ratio: 3 / 2; }
  /* The New agent tile goes. One column means it is a full-width empty box
     at the top of the list, and the first thing the frame shows should be
     a machine doing something rather than the button that would make one.
     Hidden, not deleted: it is still the desktop's first tile, and its
     label stays in the DOM for the translation pass. */
  .ab-shot__new { display: none; }

  /* The bar has no business in the intro's opening beat: there the frame is
     transparent and the composer is alone on the screen, and a tab bar
     drawn over it lands right across the sentence being typed. */
  .ab-shot.solo .ab-shot__tabs { display: none; }
  .ab-shot__tabs { display: flex; position: absolute; left: 7px; right: 7px; bottom: 7px;
    /* Over the sheet, never under it: the bar is what keeps switching
       reachable while a sheet is open, which is the reason the app raises
       it above them too. */
    z-index: 6; justify-content: space-around; align-items: stretch;
    background: var(--s-page); border-radius: 13px; padding: 6px 8px; }
  .ab-shot__tab { flex: 1; display: flex; align-items: center; justify-content: center;
    padding: 7px 0; color: var(--s-text); opacity: 0.45; }
  .ab-shot__tab.is-on { opacity: 1; }
  .ab-shot__tab svg { width: 19px; height: 19px; display: block; }
  /* The grid clears the bar the way the app's .content does — from what is
     actually there, not a guessed number. */
  .ab-shot__main { padding-bottom: 46px; }
}

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

@media (max-width: 900px) {
  /* One column, and no minimum: a 230px card holding two lines is a card
     with a hand's width of nothing under it. */
  .ab-why { grid-template-columns: 1fr; max-width: 46ch; }
  .ab-why__item { min-height: 0; padding: var(--space-3); }
  .ab-why__no { margin-bottom: var(--space-3); }
}

/* ---- The intro on a phone --------------------------------------------
   Same three beats, told the way a phone tells them. There is no room to
   open columns beside anything, so the chat is a screen of its own laid
   over the dashboard, and the hand-off is a push: the chat leaves to the
   left as the dashboard arrives from the right. That is the gesture the
   platform already uses for going forward, so it needs no explaining.

   This block sits AFTER the desktop intro rules on purpose. A media query
   adds no specificity, and every rule here ties with one up there — put it
   first and the phone silently keeps the desktop's layout.

   The panel is absolutely positioned, which also keeps the grid at its
   final two tracks throughout: the desktop collapse writes three, and a
   track list only interpolates against one of the same length. */
@media (max-width: 900px) {
  .ab-shot { position: relative; }
  .ab-shot.intro .ab-shot__panel { display: flex; position: absolute;
    inset: 7px; z-index: 4;
    transition: transform 620ms var(--ease-in-out),
                opacity var(--dur-4) var(--ease-out-expo); }
  .ab-shot.intro.solo .ab-shot__rail,
  .ab-shot.intro.solo .ab-shot__main { opacity: 0; }
  .ab-shot.intro .ab-shot__rail,
  .ab-shot.intro .ab-shot__main { transform: translateX(14%);
    transition: transform 620ms var(--ease-in-out),
                opacity 500ms var(--ease-out-expo) 120ms; }
  .ab-shot.intro:not(.solo) .ab-shot__rail,
  .ab-shot.intro:not(.solo) .ab-shot__main { transform: none; }
  .ab-shot.intro:not(.solo) .ab-shot__panel { transform: translateX(-28%);
    opacity: 0; pointer-events: none; }

  /* ── and the phone turns that push into a sheet ──────────────────────
     AFTER the 900 rules, for the reason the block above states about
     itself: a media query adds no specificity, so between two rules that
     tie, the later one wins. Placed before them these four were inert and
     the panel still slid in from the right at 390px.

     On the desktop the panel pushes the content from the right. At this
     width its own 320px is most of the screen, so a push moves the content
     off it entirely; it rises from the bottom instead, over everything
     except the tab bar. */
  @media (max-width: 720px) {
    .ab-shot.intro .ab-shot__panel { inset: 7px; z-index: 5; }
    .ab-shot.intro .ab-shot__rail,
    .ab-shot.intro .ab-shot__main { transform: none; }
    .ab-shot.intro:not(.solo) .ab-shot__panel { transform: translateY(100%); opacity: 0; }
    .ab-shot.intro.solo .ab-shot__panel { transform: translateY(0); }
  }

  /* the composer owns the width here — half of a phone is not an input */
  .ab-shot.solo .ab-shot__ask,
  .ab-shot.solo .ab-shot__chat { width: 100%; }

  /* It grows down the way a phone composer does, so the request stays
     readable instead of being clipped. Once it has more than one line a
     999px radius reads as a lozenge rather than a field, and the controls
     belong on the last line, not floating beside the middle of the text. */
  .ab-shot.compose .ab-shot__ask { min-height: 0; border-radius: var(--radius-xl);
    align-items: flex-end; gap: 12px; padding: 14px 14px 14px 18px; }
  .ab-shot.compose .ab-shot__ph { font-size: 14px; white-space: normal;
    overflow: visible; text-overflow: clip; line-height: 1.4;
    align-self: stretch; }
  .ab-shot.compose .ab-shot__send { width: 34px; height: 34px; }
  .ab-shot__send { flex: none; }               /* never squashed by the text */
  .ab-shot.compose .ab-shot__ask > svg { flex: none; }
}

/* :nth-child(n) below matches every card and costs exactly what the per-card
   rules further up cost (0,5,0). Without it those rules win here — a media
   query adds no specificity — and six of the eight cursors would keep moving
   for someone who asked the operating system for no motion. */
@media (prefers-reduced-motion: reduce) {
  .ab-shot.is-live .ab-shot__body,
  .ab-shot.is-live .ab-shot__row i.f,
  .ab-shot.is-live .ab-shot__row i.a,
  .ab-shot.is-live .ab-shot__card:nth-child(n) .ab-shot__ptr,
  .ab-shot.is-live .ab-shot__card:nth-child(n) .ab-shot__ptr i,
  .ab-shot__typing i { animation: none; }
  .ab-shot__ptr { opacity: .4; }
}

@media (max-width: 720px) {
  .ab-reel__row + .ab-reel__row { margin-top: 5px; }
  .ab-reel__row { gap: 5px; }
  /* The desktop clamps all bottom out on their floors here, and those
     floors are sized for a desktop: a 500px-wide view panel is wider than
     the phone it is on. Re-clamped against the narrow viewport, keeping
     the same two ratios. */
  .ab-panel { height: clamp(260px, 46vw, 340px); }
  .ab-panel--view { width: clamp(300px, 62vw, 480px); }
  .ab-panel--chat { width: clamp(128px, 25vw, 190px); }
}

@media (prefers-reduced-motion: reduce) {
  .ab-reel.is-live .ab-reel__row--a, .ab-reel.is-live .ab-reel__row--b { animation: none; }
  .ab-band__chat .ab-panel__who, .ab-band__chat .ab-panel__said { transition: none; }
}

@media (max-width: 900px) {
  /* Stacked, centred, and the card runs the full column.
     An earlier pass claimed centring here would be inert. It was inert as
     it had been written — on .ab-what__copy, which the child rules outrank
     — but naming the same children the base rules name settles it on source
     order, and this query comes after them. text-align-last has to be
     restated for the same reason it did there: components.css sets both, and the
     last line follows its own property.

     No 460px cap any more: the column is already the section's own
     --space-3 gutter, so dropping it lands the card on exactly the width
     #start's card has, which is what "the same padding" means here. */
  .ab-what { grid-template-columns: 1fr; gap: var(--space-6); }
  .ab-what__copy .landing__headline,
  .ab-what__copy .landing__text { text-align: center; text-align-last: center;
    margin-left: auto; margin-right: auto; }
  /* --space-2, not --space-3: at the wider card the 24px inset was giving
     the drawing 294 of 342. The frame is the point, not its margin. */
  .ab-wire-card { max-width: none; margin: 0; padding: var(--space-2); }
}

@media (prefers-reduced-motion: reduce) {
  /* One agent up, the wires quiet. A diagram, not a loop. */
  /* No entrance and no loops: the section is simply present. Every
     transition is named because .reveal's own is already off above, and
     these are the ones this block added. */
  .ab-what__copy .landing__headline,
  .ab-what__copy .landing__text,
  .ab-what-frame, .ab-wire__box, .ab-wire__window, .ab-wire__out {
    opacity: 1; transform: none; transition: none; }
  .ab-wire__bed { stroke-dasharray: none; stroke-dashoffset: 0; transition: none; }
  .ab-wire__reel, .ab-wire__pulse, .ab-wire__face { animation: none; }
  /* One face per tile, or three names would sit on top of each other. */
  .ab-wire__face { opacity: 0; }
  .ab-wire__out .ab-wire__face:first-of-type { opacity: 1; }
  /* Parked one slot in, so a name sits in the connected middle rather than
     the reel resting with its first cell in the top slot. */
  .ab-wire__reel { transform: translateY(-58px); }
  .ab-wire__pulse { opacity: 0; }
}

@media (max-width: 720px) {
  .ab-start__cap-item > span { padding: 11px 18px; font-size: 14px; }
}

@media (max-width: 720px) {
  .ab-start { margin-top: var(--space-6); }
  .ab-start__pin { padding: 0 var(--space-3); }
}

@media (max-width: 720px) {
  /* Stacked, and the drop leads: on a phone the column of chips under a
     square is a list, and a square under a column of chips is an
     afterthought. */
  .ab-ui__mach { flex-direction: column; gap: 0.8em; }
  .ab-ui__guide { order: 2; align-items: center; text-align: center; padding: 0; }
  .ab-ui__face { order: 1; width: min(40%, 32svh); }
  .ab-ui__gask { font-size: 1.35em; }
}

@media (max-width: 720px) {
  /* Stacked, and the panel gets a row of its own rather than a ceiling. At
     max-height it was handed whatever the screen left over, which was one
     message and half of the reply. */
  .ab-ui__mach { grid-template-columns: 1fr; grid-template-rows: 1.15fr 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  .ab-ui__ptr { display: none; }
  .ab-ui [data-beat] { transition: none; }
}

@media (max-width: 720px) {
  /* The two Resources boxes stop fitting side by side long before the card
     does; stacked, the arithmetic under each one stays on two lines. */
  .ab-ui__pair { flex-direction: column; width: min(24em, 82%); }
  .ab-ui__ask { font-size: 1.9em; }
}

@media (max-width: 720px) {
  /* One column, and the panel stops sticking: on a phone a sticky sidebar
     is just a block that eats most of the screen for the whole scroll. It
     lands once, after the three sections, as their closing image. */
  .ab-band { grid-template-columns: 1fr; padding: 0 var(--space-3); }
  .ab-band__head, .ab-band__flow { grid-column: 1; grid-row: auto; }
  .ab-band__flow { gap: var(--space-6); }
  /* Stacked, so nothing has to line up with anything: the card grows to
     its content and the canvas just needs a floor. */
  .ab-band__card { height: auto; }
  .ab-panel--band-view { min-height: 46svh; }
  .ab-band__chat { grid-column: 1; grid-row: auto; position: static; height: 380px; }
  .ab-band__rail { display: none; }
}

@media (max-width: 900px) {
  /* Two columns, and the wide cells give up their span. A 2-of-2 span would
     still fit, but cell 5 could not sit beside cell 4 and the row above it
     would keep a hole; every cell square makes the flow gapless again. The
     ninth is odd, so it takes the last row on its own as a wide one. */
  .ab-bento { grid-template-columns: repeat(2, 1fr); padding: 0 var(--space-3); }
  .ab-bento__cell--wide { grid-column: span 1; aspect-ratio: 1 / 1.12; }
  .ab-bento__cell:last-child { grid-column: span 2; aspect-ratio: 2 / 1; }
}

@media (max-width: 900px) {
  /* One column, not two: an odd card left alone on the second row reads as
     the leftover one, whichever it is. */
  .ab-price { grid-template-columns: 1fr; gap: 10px; }
  .ab-price__card { padding: var(--space-3); }
}

@media (max-width: 720px) {
  .ab-dev { padding: var(--space-3); margin-top: var(--space-6); }
  .ab-dev__top { flex-wrap: wrap; }
}

@media (prefers-reduced-motion: reduce) {

  .landing.lead-in .ab-mark,
  .landing.lead-in .landing__lead,
  .landing.lead-in .landing__hero .landing__text--intro,
  .landing.lead-in .landing__hero .landing__cta .btn,
  .landing.lead-in .landing__hero .landing__cta .btn:first-child,
  .landing.lead-in .landing__hero .landing__cta .btn:last-child,
  .landing.lead-in .ab-shotwrap { animation: none; }
  /* every machine shows as live rather than cycling */
}

/* AFTER THE 900 BLOCK, and that is the whole of it. A media query adds no
   specificity, so the narrower rule has to come second or the wider one wins
   inside it — the tablet's two-column square grid was rendering on a 320px
   phone, which is the thing the note below says it exists to prevent. It was
   right until a script that moved 39 dead blocks to the foot moved the 900
   past this one. */

@media (max-width: 640px) {
  /* One column and no ratio at all: a square on a 320px phone is 320px of
     mostly empty card. Content sets the height. */
  .ab-bento { grid-template-columns: 1fr; gap: 10px; }
  .ab-bento__cell,
  .ab-bento__cell--wide,
  .ab-bento__cell:last-child { grid-column: span 1; aspect-ratio: auto; padding: var(--space-3); }
}


/* ---------------------------------------------------------------------------
   NO ENTRANCE.

   The hero opened on a camera move and a staggered rise. Both are gone and the
   page paints in its finished state, so everything the intro owned has to be
   revealed by something else — this.

   Three stylesheets parked pieces of the hero off-screen for it: components.css
   holds the intro line, the CTA buttons and each word of the headline, and the
   rule near the top of this file held the mark and the mock. Nothing reveals
   them now, so they rest visible.

   #landing outranks every class selector involved, so none of this needs
   !important. shot-in and lead-in are still added by the script — some rules
   still read them — they simply no longer animate anything.

   Scoped to the hero on purpose: the looping mock inside .ab-shot (the scan,
   the caret, the pointer) is the product demonstrating itself, not an entrance,
   and it keeps running. */
#landing .ab-mark,
#landing .ab-shotwrap,
#landing .landing__lead,
#landing .landing__lead .w__i,
#landing .landing__hero .landing__text--intro,
#landing .landing__hero .landing__cta .btn {
  opacity: 1;
  transform: none;
  transition: none;
  animation: none;
}

/* ---- Why: three small live screens ---------------------------------- *
 * Each card runs the actual sequence its sentence describes, drawn from the
 * app's own screens: the agent's Configurations list opening Export this
 * agent; the Agents grid opening one agent onto what makes it that agent;
 * New agent → Import → the thing powering on.
 *
 * The values below are the APP's tokens, not the site's — design-system.css
 * in the app repo, the "measured" half of its palette. The mock is a picture
 * of that product and should be its colours; the site's own tokens are for
 * the page around it. Both halves are here so the mock still answers
 * html[data-theme], the way .ab-shot above does.
 *
 * All CSS on one 15s clock, gated on .ab-why.is-live. Three screens
 * cross-fade in place and a pointer travels between the two beats that
 * matter. Nothing runs off screen. */
.ab-wui {
  --a-canvas: #f5f5f5;  /* p-snow-50  */
  --a-panel:  #ffffff;  /* p-snow-0   */
  --a-fill:   #ededed;  /* p-snow-90  */
  --a-strong: #eaeaea;  /* p-snow-100 */
  --a-ink:    #191919;  /* p-ink-890  */
  --a-muted:  #616161;  /* p-muted-on-light */
  --a-faint:  #6b6b6b;  /* p-faint-on-light */
  --a-violet: #5568ea;  /* p-violet-500 */
  position: relative; overflow: hidden;
  margin: 0 0 auto;
  height: 186px; border-radius: 14px;
  background: var(--a-canvas); color: var(--a-ink);
  font-size: 9.5px; line-height: 1.3;
}
html[data-theme="dark"] .ab-wui {
  --a-canvas: #101010; --a-panel: #1a1a1a; --a-fill: #2b2b2b; --a-strong: #3c3c3c;
  --a-ink: #ededed; --a-muted: #ababab; --a-faint: #929292; --a-violet: #758af0;
}

/* the screens sit on top of one another and cross-fade */
.ab-wui__sc { position: absolute; inset: 0; padding: 10px 12px;
  display: flex; flex-direction: column; gap: 5px; opacity: 0; }
.ab-wui__sc--a { opacity: 1; }

/* chrome: back chevron and a centred title, as the app draws it */
.ab-wui__nav { position: relative; display: block; text-align: center;
  padding: 1px 0 7px; }
.ab-wui__nav b { font-weight: 400; font-size: 12px; letter-spacing: -.01em; }
.ab-wui__back { position: absolute; left: 0; top: 0; font-style: normal;
  color: var(--a-muted); font-size: 12px; line-height: 1; }
.ab-wui__h1 { font-size: 13px; font-weight: 400; letter-spacing: -.01em; padding-bottom: 4px; }

/* label / value rows */
.ab-wui__kv { display: flex; justify-content: space-between; gap: 8px; padding: 1px 0; }
.ab-wui__kv i { font-style: normal; color: var(--a-muted); }
.ab-wui__kv b { font-weight: 400; }

.ab-wui__cfglab { color: var(--a-muted); padding: 5px 0 1px; }

/* the Configurations rows: a fill, a label, a chevron */
.ab-wui__cfg { display: flex; align-items: center; justify-content: space-between;
  background: var(--a-fill); border-radius: 7px; padding: 5px 8px; }
.ab-wui__cfg i { font-style: normal; color: var(--a-faint); }
.ab-wui__cfg--hot { transition: background 200ms ease; }

/* the segmented control on the export screen */
.ab-wui__seg { display: flex; gap: 2px; background: var(--a-fill);
  border-radius: 999px; padding: 2px; }
.ab-wui__seg i { flex: 1; font-style: normal; text-align: center;
  padding: 4px 2px; border-radius: 999px; color: var(--a-muted); }
.ab-wui__seg i.on { background: var(--a-strong); color: var(--a-ink); }
.ab-wui__quiet { color: var(--a-muted); padding-top: 2px; }

/* the filled action, bottom right, the way Export and Open sit */
.ab-wui__cta { align-self: flex-end; margin-top: auto;
  background: var(--a-strong); color: var(--a-ink);
  border-radius: 8px; padding: 5px 14px; }
html[data-theme="dark"] .ab-wui__cta { background: #a8a8a8; color: #151515; }

/* the Agents grid */
.ab-wui__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.ab-wui__new { display: grid; place-items: center; font-style: normal; font-size: 14px;
  color: var(--a-muted); background: var(--a-fill); border-radius: 9px; aspect-ratio: 1.15; }
.ab-wui__card { position: relative; display: block; font-style: normal;
  border-radius: 9px; overflow: hidden; background: var(--a-panel); aspect-ratio: 1.15;
  box-shadow: inset 0 0 0 1px rgba(128,128,128,.18); }
.ab-wui__thumb { position: absolute; inset: 0 0 26px; display: block; }
.ab-wui__thumb--books { background: linear-gradient(160deg, #eaf3ff, #ffffff 60%); }
.ab-wui__thumb--chart { background: linear-gradient(160deg, #fff1e6, #ffffff 60%); }
.ab-wui__thumb--boot  { background: var(--a-fill); display: grid; place-items: center; }
.ab-wui__card s { position: absolute; left: 6px; bottom: 7px; text-decoration: none;
  font-size: 8.5px; }
.ab-wui__pwr { position: absolute; right: 6px; bottom: 7px; width: 9px; height: 9px;
  border-radius: 50%; box-shadow: inset 0 0 0 1.4px var(--a-violet); }
.ab-wui__pwr--on { background: var(--a-violet); }
.ab-wui__card--hot { transition: box-shadow 200ms ease; }

/* the boot ring on the powering-on card */
.ab-wui__ring { width: 14px; height: 14px; border-radius: 50%;
  border: 1.6px solid rgba(128,128,128,.3); border-top-color: var(--a-violet); }
.ab-wui__boot { color: var(--a-muted); text-align: center; padding-top: 3px; }

/* "How do you want to start?" and its three choices */
.ab-wui__ask { text-align: center; font-size: 12px; padding: 10px 0 6px; }
.ab-wui__pick { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.ab-wui__pick i { font-style: normal; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 5px; font-size: 12px;
  background: var(--a-fill); border-radius: 9px; padding: 12px 4px; color: var(--a-ink); }
.ab-wui__pick s { text-decoration: none; font-size: 8.5px; color: var(--a-muted); }
.ab-wui__pick--hot { transition: background 200ms ease; }

/* the import picker's rows */
.ab-wui__file { display: flex; justify-content: space-between; gap: 8px;
  background: var(--a-fill); border-radius: 7px; padding: 5px 8px; }
.ab-wui__file i { font-style: normal; }
.ab-wui__file b { font-weight: 400; color: var(--a-muted); }

/* what an agent carries */
.ab-wui__apps { display: flex; gap: 4px; flex-wrap: wrap; }
.ab-wui__apps i { font-style: normal; background: var(--a-fill); border-radius: 999px;
  padding: 3px 8px; color: var(--a-muted); }

/* the pointer */
.ab-wui__ptr { position: absolute; width: 13px; height: 13px; left: 0; top: 0;
  opacity: 0; pointer-events: none; }
.ab-wui__ptr svg { width: 100%; height: 100%; display: block; }

/* ---- one 15s clock ------------------------------------------------- */
.ab-why.is-live .ab-wui__sc--a { animation: w-sc-a 15s ease-in-out infinite; }
.ab-why.is-live .ab-wui__sc--b { animation: w-sc-b 15s ease-in-out infinite; }
.ab-why.is-live .ab-wui__sc--c { animation: w-sc-c 15s ease-in-out infinite; }
/* two-screen cards hold the second one to the end instead of handing on */
.ab-why.is-live .ab-wui--dist .ab-wui__sc--b,
.ab-why.is-live .ab-wui--spec .ab-wui__sc--b { animation-name: w-sc-b2; }

@keyframes w-sc-a  { 0%,30% { opacity: 1; } 36%,100% { opacity: 0; } }
@keyframes w-sc-b  { 0%,32% { opacity: 0; } 38%,62% { opacity: 1; } 68%,100% { opacity: 0; } }
@keyframes w-sc-b2 { 0%,32% { opacity: 0; } 38%,92% { opacity: 1; } 98%,100% { opacity: 0; } }
@keyframes w-sc-c  { 0%,64% { opacity: 0; } 70%,92% { opacity: 1; } 98%,100% { opacity: 0; } }

/* the row the pointer is about to press lights first */
.ab-why.is-live .ab-wui__cfg--hot,
.ab-why.is-live .ab-wui__pick--hot { animation: w-arm 15s ease-in-out infinite; }
@keyframes w-arm { 0%,18% { background: var(--a-fill); }
                   24%,30% { background: var(--a-strong); }
                   36%,100% { background: var(--a-fill); } }
.ab-why.is-live .ab-wui__card--hot { animation: w-armcard 15s ease-in-out infinite; }
@keyframes w-armcard { 0%,18% { box-shadow: inset 0 0 0 1px rgba(128,128,128,.18); }
                       24%,30% { box-shadow: inset 0 0 0 1.6px var(--a-violet); }
                       36%,100% { box-shadow: inset 0 0 0 1px rgba(128,128,128,.18); } }

/* the pointer walks to the thing it presses, presses, and leaves */
.ab-why.is-live .ab-wui__ptr { animation: w-ptr 15s ease-in-out infinite; }
.ab-why.is-live .ab-wui--spec .ab-wui__ptr { animation-name: w-ptr-spec; }
.ab-why.is-live .ab-wui--simp .ab-wui__ptr { animation-name: w-ptr-simp; }

@keyframes w-ptr {           /* Export this agent, the fifth row down */
  0%   { opacity: 0; transform: translate(210px, 168px); }
  6%   { opacity: 1; }
  22%  { opacity: 1; transform: translate(96px, 128px); }
  26%  { transform: translate(96px, 130px) scale(.88); }
  30%  { transform: translate(96px, 128px); }
  40%  { opacity: 1; transform: translate(96px, 128px); }
  50%  { opacity: 0; transform: translate(210px, 168px); }
  100% { opacity: 0; transform: translate(210px, 168px); }
}
@keyframes w-ptr-spec {      /* the second card in the grid */
  0%   { opacity: 0; transform: translate(210px, 168px); }
  6%   { opacity: 1; }
  22%  { opacity: 1; transform: translate(148px, 66px); }
  26%  { transform: translate(148px, 68px) scale(.88); }
  30%  { transform: translate(148px, 66px); }
  40%  { opacity: 1; transform: translate(148px, 66px); }
  50%  { opacity: 0; transform: translate(210px, 168px); }
  100% { opacity: 0; transform: translate(210px, 168px); }
}
@keyframes w-ptr-simp {      /* Import, then Open */
  0%   { opacity: 0; transform: translate(210px, 168px); }
  6%   { opacity: 1; }
  22%  { opacity: 1; transform: translate(150px, 104px); }
  26%  { transform: translate(150px, 106px) scale(.88); }
  30%  { transform: translate(150px, 104px); }
  44%  { opacity: 1; transform: translate(150px, 104px); }
  54%  { opacity: 1; transform: translate(238px, 146px); }
  58%  { transform: translate(238px, 148px) scale(.88); }
  62%  { opacity: 1; transform: translate(238px, 146px); }
  70%  { opacity: 0; transform: translate(210px, 168px); }
  100% { opacity: 0; transform: translate(210px, 168px); }
}

.ab-why.is-live .ab-wui__ring { animation: w-spin 1.1s linear infinite; }
@keyframes w-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .ab-why.is-live .ab-wui * { animation: none !important; }
  .ab-wui__sc--b, .ab-wui__sc--c { opacity: 0; }
  .ab-wui__sc--a { opacity: 1; }
}

@media (max-width: 900px) { .ab-wui { height: 176px; } }


/* ---- corrections, second pass --------------------------------------- */

/* The window the agent is working in, and the agent's own cursor moving
   over it. Specialization is not a list of facts about an agent, it is the
   agent doing the thing only it can do — so opening one lands on the work
   rather than on a spec sheet. Same idea as the hero mock's named pointers. */
.ab-wui__win { position: relative; display: block; flex: 1;
  background: var(--a-panel); border-radius: 8px; overflow: hidden; }
.ab-wui__winbar { display: flex; gap: 3px; padding: 5px 6px; }
.ab-wui__winbar u { width: 4px; height: 4px; border-radius: 50%;
  background: rgba(128,128,128,.35); }
.ab-wui__page { display: flex; flex-direction: column; gap: 5px; padding: 2px 8px 8px; }
.ab-wui__pr { display: flex; gap: 4px; align-items: center; }
.ab-wui__pr i { height: 4px; border-radius: 2px; background: rgba(128,128,128,.22); }
.ab-wui__pr i.w { width: 34%; }
.ab-wui__pr i.n { width: 18%; }
.ab-wui__pr i.hi { width: 46%; background: var(--a-violet); opacity: .55; }

.ab-wui__aptr { position: absolute; left: 0; top: 0; display: flex; align-items: flex-start;
  gap: 2px; pointer-events: none; }
.ab-wui__aptr svg { width: 11px; height: 11px; display: block; }
.ab-wui__aptr b { font-weight: 400; font-size: 7.5px; padding: 1px 5px;
  border-radius: 999px; background: var(--a-violet); color: #fff; white-space: nowrap; }

/* the pick row's icons, now drawn rather than borrowed from a font */
.ab-wui__ic { width: 15px; height: 15px; display: block; color: var(--a-ink); }

/* ---- three clocks, not one ------------------------------------------ *
 * They ran on one 15s loop and the row blinked as a unit — three cards
 * changing screens on the same frame reads as a glitch, not as three
 * things happening. Same keyframes, three durations: the proportions hold
 * and they drift apart on their own. Simplicity gets the longest because
 * it has three beats to the others' two. */
.ab-why.is-live .ab-wui--dist * { animation-duration: 12s; }
.ab-why.is-live .ab-wui--spec * { animation-duration: 14.5s; }
.ab-why.is-live .ab-wui--simp * { animation-duration: 18s; }
/* the boot ring keeps its own speed — it is a spinner, not a beat */
.ab-why.is-live .ab-wui__ring { animation-duration: 1.1s; }
/* the agent's cursor travels on its own, unrelated to the card's clock */
.ab-why.is-live .ab-wui__aptr { animation: w-work 6.5s ease-in-out infinite; }
@keyframes w-work {
  0%   { transform: translate(34px, 34px); }
  22%  { transform: translate(120px, 52px); }
  44%  { transform: translate(58px, 78px); }
  66%  { transform: translate(150px, 96px); }
  88%  { transform: translate(70px, 44px); }
  100% { transform: translate(34px, 34px); }
}

/* THE POINTER HAS TO LAND ON THE BUTTON. The Open press was aimed at
   coordinates the button never occupies: .ab-wui__cta is align-self:flex-end
   with margin-top:auto, so it sits at the bottom RIGHT of a 186px frame, not
   where the second beat was pointing. Measured against the rendered card. */
@keyframes w-ptr-simp {
  0%   { opacity: 0; transform: translate(150px, 176px); }
  5%   { opacity: 1; }
  18%  { opacity: 1; transform: translate(152px, 100px); }   /* Import */
  22%  { transform: translate(152px, 102px) scale(.88); }
  26%  { transform: translate(152px, 100px); }
  38%  { opacity: 1; transform: translate(152px, 100px); }
  52%  { opacity: 1; transform: translate(268px, 152px); }   /* Open */
  56%  { transform: translate(268px, 154px) scale(.88); }
  60%  { opacity: 1; transform: translate(268px, 152px); }
  70%  { opacity: 0; transform: translate(150px, 176px); }
  100% { opacity: 0; transform: translate(150px, 176px); }
}
