/* ==========================================================================
   aiRA — /product (20 Sep): the depth the homepage compresses. Hero with the
   real product capture, then the mechanism, the seven capabilities as their
   real /lp question strings, the category board, governance, and the book
   box. The mechanism, comparison and book-box sections are the shared blocks
   in components.css; this file carries the hero, the capabilities grid, and
   self-contained copies of the feat-icon and sec anatomies (those rules live
   fused with homepage-only selectors in home.css, so they are restated here
   the way industry.css restates the caption icons).

   Loads after tokens -> global -> components.
   ========================================================================== */

/* ---- Hero: centred, product below (22 Sep, sendr.ai reference) ----------
   Copy centred and narrow, then the capture full width directly under it,
   running past the fold so the app reads as already open rather than
   pictured. It was a left/right split for a few hours before this, and a
   centred stack over a drawn loop diagram before that.

   The capture also absorbed the #demo section that briefly held it: the
   product belongs in the hero, not in a section of its own underneath one.
   -------------------------------------------------------------------------- */

/* ONE COLUMN, and the copy and the glyph field share its first cell (23 Sep,
   user: give .phero__copy the same max-width as /solutions' .hero-ind__copy).
   The cap is the only reason the grid exists. .phero__copy used to be the
   field's containing block AND the full width of the container, so capping it
   at 46rem took the field down with it from 1240 to 736, and at 736 the field
   has nowhere to be: the canvas clears an ellipse of 0.2816 x its own box to
   keep glyphs off the headline, which is 349px at 1240 and matches the H1's
   350px half exactly, but only 207px at 736, so the glyphs would run straight
   through the words. Sharing a cell keeps the field on the COLUMN's width
   while the copy is capped, and keeps its block box the copy's own row, which
   is what the -section-pad / -sp-16 margins are measured against. */
.phero .container {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  text-align: center;
}

/* The centred block's two measures are SET DIRECTLY, 700 on the H1 and 600 on
   the sub (user, 23 Sep). They shared one fluid measure for a day, and what
   that was solving is worth keeping written down because a flat px measure
   brings it back:

   --phero-measure was calc(var(--t-display-0) * 11.73), derived from the
   headline's own clamp so the box shrank with the type. A measure that does
   NOT move with the font gives the H1 a band where it falls to two lines with
   the SECOND longer than the first, which in a centred block is an inverted
   taper. At 700px that band is roughly 640 to 880: measured, 900 gives three
   lines at 447/418/386 and 830 gives two at 535/670. Below 640 the container
   is narrower than the measure anyway and it goes back to three.

   A breakpoint cannot fix it, because the flip is driven by the fluid font
   and any breakpoint leaves a bad band on one side of it. What does fix it is
   capping the fluid measure rather than replacing it:
     max-width: min(700px, calc(var(--t-display-0) * 10.94));
   which is exactly 700 everywhere the clamp is at its 64px ceiling and
   shrinks with the type below that. Not applied: 700 flat is what was asked
   for. */

/* ---- The glyph field (22 Sep, user reference: sv-animations GlyphMatrix) --
   A canvas of small mono glyphs behind the copy, a few of which change every
   150ms: two thirds of the characters are middots and three cells in a
   hundred move per tick, so it registers as paper texture and only resolves
   into marks up close. See the colour note on the rule itself for how dark it
   ended up and why it had to.

   IT IS ANCHORED TO THE COPY, NOT THE SECTION, and that is what keeps it off
   the capture. The host is a child of .phero__copy and its box runs from the
   section's own top padding down to exactly the figure's top margin, both
   written as the same tokens those two rules use, so the field still lands on
   the figure's edge if either changes.

   The pitch is 16px because --cell is 80: see js/glyph-matrix.js, and the
   20 Sep hero lattice for what a pitch that does not divide the cell does to
   the page frame.

   Two masks, one per element, so neither has to composite: the host fades the
   field out downward before it reaches the capture, and the canvas clears an
   ellipse around the text. That ellipse is the same shape the home hero's
   lattice uses, which is what makes the two grounds read as one language. */
.phero__copy {
  grid-area: 1 / 1;
  /* 800px since 28 Sep, up from the 46rem (736) set on 23 Sep to match
     /solutions' .hero-ind__copy. It HAS to move with the headline: the H1's
     own max-width went to 800 in the same pass and a 736px parent would have
     silently capped it back, which is the failure mode of setting a measure on
     a child of a narrower box. The sub stays 600 flat, so the block is still
     one column of a stated width rather than three measures loose in a
     full-width box, which is what this cap is for. */
  /* width:100% as well as the cap, because an auto-margined GRID item is
     shrink-to-fit: without it the block collapses onto its widest child and
     comes out 700 (the H1's own measure) rather than the 736 asked for, which
     is a different box from the one .hero-ind__copy makes in normal flow. */
  width: 100%;
  max-width: 800px;
  margin-inline: auto;
  /* Paints over the field: same cell, later in the DOM, and positioned. */
  position: relative;
}

.phero__matrix {
  grid-area: 1 / 1;
  /* Stretched to the cell, then pushed out top and bottom by exactly the two
     tokens the neighbouring rules use: up through the section's own top
     padding, and down to where .phero__fig's margin-top begins. Written as
     margins rather than as top/bottom insets because the field is a grid item
     now, not an absolutely positioned layer. It still lands on the figure's
     edge if either of those rules changes. */
  margin-block: calc(-1 * var(--section-pad)) calc(-1 * var(--sp-16));
  /* No arithmetic: the frame's rails ride .container's outer edges and its
     20px inner padding is what keeps content clear of them, so a layer that
     simply fills .phero__copy already runs the width of the frame with that
     same 20px of air either side.

     It was a 50vw bleed with padding-inline: var(--frame-x) first, which put
     the field 18px OUTSIDE each rail: a percentage inside that token resolves
     against the containing block, .phero__copy, never against the bled box,
     so its 5% branch measured the container and its calc branch went
     negative. Do not reach for --frame-x from inside the container. */
  /* The INK itself, at 0.55. It started at --ink-3 and 0.34 and the user
     could not see it, twice (23 Sep, the second time asking in terms for a
     dark colour). The arithmetic is why: the script applies a per-cell alpha
     jitter on top of this, so the opacity here is not what lands. At --ink-3
     and 0.34 the average glyph was arriving near 21% of a mid grey on a
     near-white ground, which is under the threshold where a 12px stroke reads
     at all. Going dark rather than more opaque is what actually fixed it: a
     faint near-black glyph still has edges, a faint grey one has none. */
  color: var(--ink);
  /* The three hues of the site's own palette, in the client's own names:
     Tertiary, Secondary and Primary are one colour at three hues, 30 degrees
     either side of the brand (user, 23 Sep: "some colors teal and purple and
     brand/primary"). js/glyph-matrix.js reads these and gives a minority of
     cells one of them, so the field is ink with colour scattered through it
     rather than three fields of colour. They are the SATURATED mid steps on
     purpose: this layer already sits at 0.55 with a per-cell alpha jitter on
     top, and a pale accent under that arrives as nothing, which is the same
     lesson the field's own colour taught on 23 Sep. */
  --gm-c1: var(--tertiary-600);
  --gm-c2: var(--secondary-500);
  --gm-c3: var(--brand);
  opacity: 0.55;
  pointer-events: none;
  /* Fades in under the header and out again before the capture. Without the
     first stop the field starts on a hard horizontal edge level with the top
     of the nav card. */
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, #000 14%, #000 48%, transparent 92%);
  mask-image: linear-gradient(to bottom,
    transparent 0, #000 14%, #000 48%, transparent 92%);
}
.phero__matrix .glyphs__canvas {
  /* Sized against the H1's 700px measure: the clear radius is 0.44 x 0.64 of
     the box, so 349px at a 1240px column, against the headline's own 350px
     half. That arithmetic is why the field cannot live inside a 46rem copy
     block, and why it sits in the grid cell instead. */
  --gm-clear: 0.44;
  -webkit-mask-image: radial-gradient(64% 78% at 50% 50%,
    transparent 0 calc(var(--gm-clear) * 100%), rgba(0, 0, 0, 0.45) 76%, #000 100%);
  mask-image: radial-gradient(64% 78% at 50% 50%,
    transparent 0 calc(var(--gm-clear) * 100%), rgba(0, 0, 0, 0.45) 76%, #000 100%);
}

/* The copy paints over the field by DOM order: the host is the first child of
   .phero__copy and these are positioned siblings after it, so they win
   without a z-index. A negative one on the host would have been the other
   way, and it escapes .phero__copy the moment .reveal leaves a stacking
   context behind. */
.phero__t,
.phero__sub,
.phero__cta { position: relative; }

.phero__t {
  font-family: var(--font-display);
  /* --t-display-0, the one hero size since 28 Sep. It was --t-display-1 (60)
     while the homepage sat at 70; with every non-utility hero on 64 there is
     one token and no argument about which page gets which. */
  font-size: var(--t-display-0);
  font-weight: 500;
  line-height: 1.04;
  letter-spacing: var(--tr-display);
  color: var(--ink);
  /* 800px at full size (user, 28 Sep, raised from the 700 set on 23 Sep), but
     FLUID BELOW IT, which is the part that cannot be dropped.
     A flat 800 puts the headline on TWO lines with the second longer than the
     first everywhere from about 768 to 1050: measured, 992 gives 614 / 768 and
     900 gives 585 / 732. That is an inverted taper in a centred block, and the
     band is wide enough to cover most laptops and every tablet. The long note
     at the top of this file describes exactly this failure at the old 700px
     measure, where the bad band was 640 to 880, and describes this fix: cap a
     measure that moves WITH the type rather than replacing it. It was not
     applied then only because a flat 700 was what had been asked for.
     12.5 is 800 / 64, so the cap resolves to exactly 800 at the clamp's 64px
     ceiling and the box shrinks in step with the font below it. */
  max-width: min(800px, calc(var(--t-display-0) * 12.5));
  margin-inline: auto;
  margin-bottom: var(--sp-5);
  text-wrap: balance;
}
/* The brand highlight, same drawing as .section-heading em: restated because
   this h1 is not a section heading. */
.phero__t em {
  font-style: normal;
  color: var(--primary-400);
  text-decoration-line: underline;
  text-decoration-style: dashed;
  text-decoration-thickness: 0.07em;
  text-underline-offset: 0.16em;
  text-decoration-skip-ink: none;
}

.phero__sub {
  font-size: var(--t-body-xl);
  line-height: var(--lh-body);
  color: var(--ink-2);
  /* 600px (user, 23 Sep), which ENDS the measure this shared with the H1. That
     was set on 22 Sep ("the h1 and subhead should have same width") and the
     headline is 700 and the sub is narrower than it rather than equal to it,
     which is a deliberate reversal and not a drift. Flat rather than fluid,
     so it also stops tracking the headline's clamp. */
  max-width: 600px;
  margin-inline: auto;
  /* Centred copy is read by its rag, and greedy wrapping gave this one
     621 / 697 / 668: the longest line in the MIDDLE, which is the shape that
     reads as "out and in". Balanced it runs 621 / 677 / 688, which only ever
     grows, and a rag that moves one way is not read as a rag at all. */
  text-wrap: balance;
}

.phero__cta {
  justify-content: center;
  margin-top: var(--sp-8);
}

/* ---- The capture --------------------------------------------------------
   aspect-ratio on the FRAME, not the image, so swapping the <img> for the
   Guideflow <iframe> changes nothing: an iframe has no intrinsic size and
   would otherwise collapse the hero.

   This one keeps a hairline and a radius where the previous hero capture had
   neither. That asset arrived already sitting on its own rounded card, so a
   frame there was a frame around a frame; this is a raw app capture and needs
   an edge of its own.
   -------------------------------------------------------------------------- */
.phero__fig {
  grid-area: 2 / 1;
  position: relative;
  margin-top: var(--sp-16);
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  /* Two layers: a wide soft one to lift it off the page and a tight one to
     keep the edge from going mushy. The sendr reference's one real trick. */
  box-shadow: 0 32px 64px -16px rgba(11, 16, 32, 0.16),
              0 2px 8px rgba(11, 16, 32, 0.04);
}
/* THE CAPTURE SETTLES INTO PLACE ON SCROLL (user, 28 Sep). It starts tilted
   back and slightly small, and stands up to its resting position as it comes
   into view.

   IT IS A SCROLL-DRIVEN CSS ANIMATION, not a scroll listener. animation-timeline
   is bound to the element's own position in the viewport, so it runs on the
   compositor, cannot fall behind the scroll the way a rAF handler can, and
   costs no JavaScript at all.

   THE RESTING STATE IS THE ELEMENT'S NORMAL STATE and the animation only ever
   moves AWAY from it. That is what makes the degradation free: a browser
   without scroll-driven animations (Firefox still ships it behind a flag)
   simply shows the capture where it belongs, with nothing missing and nothing
   half-applied. The @supports guard is what stops the keyframes running as an
   ordinary time-based animation on load in those browsers.

   IT TRANSFORMS .phero__fig, NOT THE IMG. The img is inside overflow:hidden, so
   scaling it would only move the picture inside a frame that stayed put; the
   frame, its border, its radius and its two shadows all have to travel
   together for this to read as one object.

   PERSPECTIVE IS A TRANSFORM FUNCTION here rather than a property on the
   parent: .phero__fig's parent is the hero's grid, which also holds the copy
   and the glyph field, and giving that a 3D context would drag both into it.
   It has to appear in BOTH keyframes or the two transform lists do not
   interpolate and the rotation snaps.

   THE ORIGIN IS THE TOP EDGE, so the card hinges at the top and its lower half
   swings up. Origin centre reads as a card being pushed, top reads as one
   standing up, which is the gesture the reference sites use. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .phero__fig {
      transform-origin: 50% 0;
      animation: phero-settle linear both;
      animation-timeline: view();
      /* Starts a little after the top edge appears and finishes well before
         the capture is fully covered, so the last of the movement happens
         while it is comfortably on screen rather than at the edge. */
      animation-range: entry 8% cover 32%;
    }
    @keyframes phero-settle {
      from {
        opacity: 0.5;
        transform: perspective(1600px) translateY(28px) rotateX(9deg) scale(0.93);
      }
      to {
        opacity: 1;
        transform: perspective(1600px) translateY(0) rotateX(0deg) scale(1);
      }
    }
  }
}

.phero__fig > img,
.phero__fig > iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
  object-position: top center;
}

/* ---- Shared anatomies restated (see the head comment) --------------------
   The feat icon-in-title grammar and the mini grid, exactly the home.css
   drawing, for the mechanism's three points. */

.feat__icon {
  display: block;
  margin-bottom: var(--sp-3);
  color: var(--brand);
}
.feat__icon svg { width: 20px; height: 20px; }
.feat__mini h4:has(> .feat__icon) {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.feat__mini h4 > .feat__icon { display: inline-flex; margin-bottom: 0; }
.section--inv .feat__icon { color: var(--primary-200); }

.feat__mini {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-10) var(--sp-8);
  border-top: 1px solid var(--border);
  padding-top: var(--sp-10);
}
.feat__mini > li { display: flex; flex-direction: column; }
.feat__mini h4 {
  font-size: var(--t-title-sm);
  font-weight: 600;
  letter-spacing: var(--tr-title);
  color: var(--ink);
  margin-bottom: var(--sp-2);
}
.feat__mini p {
  font-size: var(--t-body-sm);
  line-height: 1.65;
  color: var(--ink-3);
}

/* ==========================================================================
   THE DARK RUN: #how and #governance share one light
   --------------------------------------------------------------------------
   The two dark sections are meant to read as one block, so the light that
   lifts the mechanism's ground has to CROSS the boundary instead of dying on
   it (user, 23 Sep: "the how it fits and security sections color should
   connect or flow through"). It was dying on it, and the reason is worth
   keeping: .mech__glow is centred at 112% of #how's OWN height, which puts
   the bright middle of the ellipse below the seam where #how cannot draw it.
   So #how ramped up to its lightest exactly at the joint and #governance
   started flat, which is the one place a step reads as two panels.

   Both halves are written from the SEAM now, in the same lengths, so they
   are one ellipse that happens to be drawn twice: its centre sits --glow-y
   below the joint, #how draws everything above that point and #governance
   everything below it. Percentages cannot do this, because they resolve
   against each section's own height and the two are never equal (889 and 900
   here, and either moves the moment a line of copy wraps).

   This is a PAGE override, not a change to .mech__glow: that component also
   runs on index.html, index-v2 and index-v3, where the mechanism is followed
   by a LIGHT section and has nothing to flow into.
   -------------------------------------------------------------------------- */
.mech,
.sec {
  /* How far below the seam the light's centre sits. */
  --glow-y: 120px;
  --glow-h: 560px;
  /* The second, smaller ellipse: off to the left and a little lower, which
     is what keeps the wash from reading as one symmetrical blob. */
  --glow2-y: 160px;
  --glow2-h: 360px;
  --glow-1: color-mix(in srgb, var(--brand) 14%, transparent);
  --glow-2: color-mix(in srgb, var(--primary-300) 8%, transparent);
}
.mech .mech__glow {
  background:
    radial-gradient(80% var(--glow-h) at 50% calc(100% + var(--glow-y)),
      var(--glow-1), transparent 72%),
    radial-gradient(46% var(--glow2-h) at 16% calc(100% + var(--glow2-y)),
      var(--glow-2), transparent 75%);
}
/* The lower half rides #governance's own BACKGROUND rather than a decor
   element. #how can use an absolutely placed layer because .mech__diagram is
   positioned and paints over it; nothing in #governance is positioned, so the
   same layer would sit on top of the copy. A background-image cannot. */
.sec {
  background-image:
    radial-gradient(80% var(--glow-h) at 50% var(--glow-y),
      var(--glow-1), transparent 72%),
    radial-gradient(46% var(--glow2-h) at 16% var(--glow2-y),
      var(--glow-2), transparent 75%);
}

/* ==========================================================================
   SECURITY (#governance)
   --------------------------------------------------------------------------
   Rebuilt 23 Sep from a laravel.com/cloud reference: the claim with a checked
   list, then a row of certification marks drawn as emblems rather than set as
   a sentence.

   DARK, and continuous with #how by design. The cert row's dividers are the
   only structure; there are no cards, which is what lets the two dark
   sections read as one block rather than as two panels.

   THE HEAD IS THE SITE'S OWN, not the reference's. The reference opens on a
   ruled strip with its label on the left and a line of engineering detail
   opposite; that was built here and came straight back out, because every
   other section on the site is eyebrow over heading and a section that
   invents its own head reads as imported. The mono line it carried is the
   first checked item below almost verbatim, so nothing went with it.

   The cert row BLEEDS to the rails, the same 20px giveback the bento uses:
   .container pads that much purely to clear the frame, so handing it back
   puts the row's rules on the frame's own verticals.
   ========================================================================== */

.sec__list {
  list-style: none;
  padding: 0;
  /* .section-head owns the gap above; the list starts at the body's top. */
  margin: 0;
  display: grid;
  gap: var(--sp-5);
  max-width: 54rem;
}
.sec__list li {
  display: flex;
  gap: var(--sp-3);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  color: var(--ink-inv-2);
}
.sec__list svg {
  flex: none;
  width: 20px;
  height: 20px;
  /* Optically on the first line rather than its box: the 20px glyph is taller
     than the 14px text's cap height. */
  margin-top: 3px;
  color: var(--primary-200);
}

/* Primary is --black, which disappears on this ground. The inverse of "the
   primary button is ink" is a light fill with ink on it, and it is the one
   element in the section that should carry full contrast. */
.section--inv .btn--primary {
  background: var(--ink-inv);
  color: var(--black);
  box-shadow: none;
}
.section--inv .btn--primary:hover {
  background: color-mix(in srgb, var(--ink-inv) 88%, var(--black));
}

/* ---- The certification row ----------------------------------------------
   Emblems, not a sentence. Six cells divided by hairlines, running rail to
   rail under the copy, each one a mark plus a wordmark stack.

   WHAT IS IN HERE IS A SUBSET ON PURPOSE. Only the claims held as
   certifications or compliance get an emblem; HIPAA-capable-under-a-BAA and
   the two AI-framework ALIGNMENTS keep their qualifier in .sec__note, because
   a badge strips one and would overstate all three.
   -------------------------------------------------------------------------- */
.sec__certs {
  list-style: none;
  padding: 0;
  margin: var(--sp-16) 0 0;
  margin-inline: calc(-1 * var(--sp-5));
  display: grid;
  /* THREE, not six, since 23 Sep: only the marks Capillary supplied are drawn,
     and the rest are carried in .sec__note. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  border-block: 1px solid var(--frame-line-inv);
}
.cert {
  position: relative;
  display: grid;
  place-items: center;
  /* Mark over label, with the pair centred as one object. */
  gap: var(--sp-4);
  align-content: center;
  min-height: 140px;
  padding: var(--sp-6) var(--sp-3);
  color: var(--ink-inv-3);
}
.cert + .cert { border-left: 1px solid var(--frame-line-inv); }

/* A SUPPLIED MARK takes the whole cell: the artwork carries its own wordmark,
   so the drawn emblem and the .cert__stack under it would both be saying it
   twice.

   SIZED BY HEIGHT, not width, and not to a box. The three are 200x200, 182x177
   and 158x200, so a common width would render the tall shield small and the
   circles large. A common height gets them closer, and the shield still takes
   a step down because a shape that is taller than it is wide reads bigger at
   the same height than a circle does; 84 against 92 is that correction, and it
   is optical, so do not "fix" it back to one number.

   They are FULL-COLOUR marks on a dark ground, which is the one place this
   site lets a logo keep its own colour: the rule that customer logos render in
   full colour applies here too, and a compliance mark recoloured to fit a
   palette stops being the mark. */
.cert__logo {
  display: block;
  width: auto;
  height: 76px;
}
.cert:last-child .cert__logo { height: 70px; }
/* The mark says what it is, the label says what it MEANS: two of the three
   are graphics rather than wordmarks, and the AICPA circle names the auditor
   rather than the standard. It is the eyebrow's own spec, so the row's type
   is the section's type. */
.cert__name {
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: var(--tr-mono);
  text-transform: uppercase;
  color: var(--ink-inv-3);
  text-align: center;
}

/* The emblem sits BEHIND the wordmark rather than above it, which is what
   makes these read as marks instead of as icons with captions. */
.cert__mark {
  position: absolute;
  /* 84 and not 72: the ring has to clear the wordmark it sits behind, and
     GDPR is the widest of them at about 50px. At 72 the stars landed on the
     letters. */
  width: 84px;
  height: 84px;
  opacity: 0.55;
  pointer-events: none;
}
.cert__stack {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 2px;
  text-align: center;
}
.cert__lg {
  font-family: var(--font-display);
  font-size: var(--t-title-sm);
  font-weight: 700;
  letter-spacing: 0.01em;
  line-height: 1;
  color: var(--ink-inv-2);
}
.cert__sm {
  font-family: var(--font-display);
  font-size: var(--t-caption);
  font-weight: 600;
  letter-spacing: 0.08em;
  line-height: 1;
  color: var(--ink-inv-3);
}

.sec__note {
  margin-top: var(--sp-6);
  font-size: var(--t-caption);
  line-height: var(--lh-body);
  color: var(--ink-inv-3);
  max-width: 44rem;
}

/* ==========================================================================
   THE WORKSPACE (#workspace): five cells, 2 / 1 / 2
   --------------------------------------------------------------------------
   Built 23 Sep from the firecrawl.dev/search reference the user supplied. It
   replaces two full-width sections, #ask and the old #features, clubbed on
   the user's call: they were the same subject read twice, once as what
   happens when you ask and once as what lands when it answers.

   EVERY ILLUSTRATION WENT with them, also the user's call, and that is most
   of what this block used to be: the drawn .thread, the .pbook registry
   panel, the four .work__fig surfaces, the .mark annotation, the accordion
   and its synced well. The reference carries no pictures in its cells either
   beyond a small glyph, and five claims that each say a specific checkable
   thing do not need illustrating.

   IT IS DRAWN IN LINES, NOT CARDS (user, 23 Sep, second pass on the same
   reference: "no padding between grid, needs to follow our lines structure").
   There is no gap, no fill, no radius and no shadow. The cells simply share
   hairlines, and the grid is pulled back out to the RAILS so its own edges
   sit on the frame's verticals rather than 20px inside them. That is the
   whole reason .container carries a 20px gutter, so undoing it is what makes
   this grid part of the page frame instead of a thing floating within it.

   The MIDDLE CELL IS WIDE because it is the load-bearing one: "it builds you
   something" is the section's argument and the four artifact kinds are its
   evidence. The two above are what happens when you ask, the two below are
   how you check what came back, so the block reads ask / build / verify.
   ========================================================================== */

/* The head shares the GRID'S internal edge, not .container's. Once the grid
   bleeds out to the rails, the section's outer edge is the frame, and the
   24px the cells pad in from it is the section's real left margin. Left on
   .container's 20px the head's text sat at x=100 against the first cell's
   104, which is an almost-alignment and reads worse than a frank inset. */
/* ONE padding for the block, so the head and the cells cannot drift apart.
   They had: the head was on --sp-6 and the cells on --sp-12, which put the
   heading's text 24px inside the rail and the cell copy 48px inside it. The
   head is given the same giveback as the grid and then pushed back in by the
   cells' own pad, so the two read as one left edge. */
.bento { --cell-pad: var(--sp-8); }

.bento__head {
  margin-inline: calc(-1 * var(--sp-5));
  padding-inline: var(--cell-pad);
  max-width: calc(46rem + var(--sp-5) + var(--cell-pad));
  margin-bottom: var(--sp-12);
}
.bento__sub { margin-top: var(--sp-5); }

.bento__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  /* SIX rows, two per band: copy then figure, copy then figure, copy then
     figure. Each cell spans its pair and takes them back with subgrid, which
     is what keeps the two cells of a band honest: their copy blocks share a
     row so the rule under them lands at one height, and their wells share a
     row so the inset lines inside them line up across the divider. Sized by
     content, so the figure rows are simply the well's own height. Without
     subgrid each cell rows itself and the only loss is that alignment. */
  grid-template-rows: repeat(6, auto);
  /* No gap: the cells meet, and the line between them is the divider. */
  gap: 0;
  /* Out to the rails. .container pads 20px purely to clear them, so giving
     that back puts this grid's outer edges exactly on the frame's verticals
     and its horizontals run rail to rail. Zeroed again below 992, where the
     rails come off and .container drops the gutter with them: keeping the
     negative margin there would push the grid outside .padding-global. */
  margin-inline: calc(-1 * var(--sp-5));
  border-block: 1px solid var(--frame-line);
}

.bento__cell {
  display: grid;
  grid-row: span 2;
  grid-template-rows: subgrid;
  /* 32px (user, 23 Sep). No bottom pad: the figure closes the cell, and it
     bleeds out to the cell's own edges to do it. */
  padding: var(--cell-pad) var(--cell-pad) 0;
  border-right: 1px solid var(--frame-line);
  border-bottom: 1px solid var(--frame-line);
}
/* The 2 / 1 / 2 arrangement is the design rather than a list that happens to
   be five long, so the closing edges are named directly. Cells 2, 3 and 5 end
   their row; 4 and 5 are the last row. The grid's own border-block draws the
   top and bottom, and the frame's rails draw the sides. */
.bento__cell:nth-child(2),
.bento__cell:nth-child(3),
.bento__cell:nth-child(5) { border-right: 0; }
.bento__cell:nth-child(4),
.bento__cell:nth-child(5) { border-bottom: 0; }

/* The eyebrow's spec with an icon in front of it and no "/" prefix: the
   section head already carries one, and six slashes in a section reads as a
   tic rather than as the mark it is. */
.bento__label {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: var(--tr-mono);
  text-transform: uppercase;
  line-height: 1;
  color: var(--ink-3);
  margin: 0 0 var(--sp-4);
}
/* Grey, inheriting the label: the brand appears once per section head, on the
   heading's highlighted word, and five lit icons would be five times more. */
.bento__label svg { width: 20px; height: 20px; flex: none; }

/* 16px/500 (user, 23 Sep), which means overriding BOTH halves of the universal
   h3 rule in global.css, 28px/600. That rule's own note allows this: a card
   title that wants to be smaller has to say so explicitly rather than omit
   the size and fall through to the browser default. At this size the claim
   and the paragraph under it are nearly the same object, which is the
   reference's whole look: the lead sentence is the emphasis, not a heading
   sitting above copy. */
.bento__t {
  margin: 0;
  font-size: var(--t-title-sm);
  font-weight: 500;
  line-height: var(--lh-title);
  letter-spacing: var(--tr-title);
  text-wrap: balance;
}
.bento__p {
  margin: var(--sp-3) 0 0;
  font-size: var(--t-body);
  line-height: var(--lh-body);
  color: var(--ink-2);
  max-width: 48ch;
}

/* ---- The figure slot -----------------------------------------------------
   Where the illustration goes, drawn as LINES ONLY (user, 23 Sep, with a
   crop of the reference): one line inset from each of the well's four edges,
   each running the full width or height of the well so they cross at the
   corners and carry on to meet the cell. That is
   the "double lines top bottom and left right" the reference shows, and it
   is the page frame's own grammar rather than a box drawn inside a box.

   The insets are 80 and 40, which are --cell and half of it: the frame's
   graph paper is pitched at 80, so these lines land where that grid would
   have put them instead of cutting across it. The lines take --frame-grid,
   not --frame-line: the cell's own borders are structure and these are
   texture, and at the same weight the well reads as four nested boxes.

   It bleeds out of the cell's padding on three sides, so its own edges meet
   the cell's borders. It carries NO RULE along its top (user, 23 Sep:
   "remove the separator line between text and image and also reduce that
   gap"): the copy and the well are one cell, and a hairline between them
   split it into two stacked blocks inside a grid already drawn in hairlines.
   With the line gone the gap is the only separation left, so it dropped from
   40 to 24, under the cell's own 32px inset.

   IT IS A RESERVED SLOT, not a decoration: it carries the site's own
   placeholder glyph (the Phosphor duotone Image at --ink-faint, the same one
   /solutions' seat shots and the industry hero use), and dropping an <img>
   in beside that glyph fills the well and stands the glyph down, with the
   lines and the well's size unchanged.

   ITS SHAPE IS A RATIO, NOT A HEIGHT (user, 23 Sep: "make it a bit square in
   height"). It was a flat 200px, which on a 640px cell is a 3.2:1 letterbox
   that no real screenshot would fit. 3/2 tracks the cell as the grid narrows
   instead of going flatter and flatter, and the cap is there for the WIDE
   cell, which is twice as wide and would otherwise want an 850px well.
   -------------------------------------------------------------------------- */
.bento__fig {
  /* 80 and 40: --cell and half of it. */
  --fig-x: 80px;
  --fig-y: 40px;
  /* The bleed is one variable feeding both the margin and the width, and it
     has to feed the width: a grid item that is stretched in BOTH axes and
     carries an aspect-ratio lets the ratio win, so the well was sizing its
     own width back off the capped height and every one of them came out 630
     wide whatever its cell. Stating the inline size makes it definite, the
     ratio then only drives the height, and max-height clamps that height
     without touching the width. */
  --fig-bleed: var(--cell-pad);
  display: grid;
  place-items: center;
  width: calc(100% + 2 * var(--fig-bleed));
  aspect-ratio: 3 / 2;
  max-height: 420px;
  margin: var(--sp-6) calc(-1 * var(--fig-bleed)) 0;
  background-image:
    linear-gradient(var(--frame-grid) 0 0),
    linear-gradient(var(--frame-grid) 0 0),
    linear-gradient(var(--frame-grid) 0 0),
    linear-gradient(var(--frame-grid) 0 0);
  background-size: 100% 1px, 100% 1px, 1px 100%, 1px 100%;
  background-position:
    0 var(--fig-y),          0 calc(100% - var(--fig-y)),
    var(--fig-x) 0,          calc(100% - var(--fig-x)) 0;
  background-repeat: no-repeat;
}
.bento__ph {
  /* Not the 20px icon size: CLAUDE.md's icon rule exempts the placeholder
     glyph in an empty media well, and at 20px in a 420px well it reads as
     debris rather than as a reserved slot. */
  width: 40px;
  height: 40px;
  color: var(--ink-faint);
}
/* Once a real shot lands it fills the well and the drawn lines stand down
   with the glyph: they are the empty state, not a frame around the art. */
.bento__fig:has(img) {
  background-image: none;
}
.bento__fig:has(img) .bento__ph { display: none; }
.bento__fig img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- Drawn art in a well ------------------------------------------------
   From contexts/UI examples (user, 23 Sep): a product fragment floating on a
   dot ground, soft shadow, sometimes a second card stacked behind. Art stands
   the drawn inset lines down exactly as a screenshot does, since they are the
   empty state, and lays a dot field in their place. The dots are pitched at
   16px, which divides --cell (80px), so they register with the frame. */
.bento__fig--art {
  position: relative;
  overflow: hidden;
  background-image: none;
  container-type: size;
}
/* Drawings sit ABOVE the dot layer: it is absolutely placed, so it would
   otherwise paint over any drawing that is not positioned itself. */
.bento__fig--art > * { position: relative; }
.bento__fig--art::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, var(--border-strong) 1px, transparent 1.4px);
  background-size: 16px 16px;
  background-position: 8px 8px;
  -webkit-mask-image: radial-gradient(ellipse 70% 70% at 50% 50%, #000 30%, transparent 100%);
  mask-image: radial-gradient(ellipse 70% 70% at 50% 50%, #000 30%, transparent 100%);
}

/* ---- One thread: the question and its answer, nothing between ---------- */
.oth {
  position: relative;
  width: 380px;
  margin: 60px 0 0 48px;      /* room for the ticket peeking out above-left */
  font-family: var(--font-product);
}
/* The ticket the same question becomes without aiRA: faded, tilted, and
   behind, so it reads as the route not taken rather than as a second
   feature. */
.oth__ticket {
  position: absolute;
  top: -76px;
  left: -64px;
  width: 272px;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  opacity: 0.8;
  transform: rotate(-4deg);
}
.oth__ticket p { margin: 0; }
.oth__ticket-h,
.oth__ticket-f {
  display: flex;
  align-items: center;
  white-space: nowrap;
  justify-content: space-between;
  gap: var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  color: var(--ink-faint);
}
.oth__ticket-t {
  margin: 6px 0 var(--sp-3) !important;
  font-size: var(--t-caption);
  font-weight: 500;
  color: var(--ink-3);
}
.oth__queued {
  padding: 0 8px;
  border-radius: var(--r-pill);
  color: var(--hold);
  background: var(--hold-bg);
}

.oth__card {
  position: relative;
  display: grid;
  gap: var(--sp-3);
  padding: var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--shadow-lg);
  text-align: left;
}
/* The person, on the right, the way the product sets its own side. It lost
   its "You" chip on 23 Sep, and the answer its aiRA sparkle: the sides are
   already set by which edge each row sits on, so the two marks were labelling
   something the layout had said. */
.oth__q {
  display: flex;
  justify-content: flex-end;
}
.oth__q .chat__q {
  margin: 0;
  max-width: 78%;
  background: var(--bg-subtle);
}
/* aiRA, on the left, answering with the artifact rather than a sentence. */
.oth__a { display: flex; }
.oth__kpi {
  flex: 1;
  display: grid;
  gap: 4px;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
.oth__kpi-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-4);
}
.oth__spark { width: 120px; height: 32px; flex: none; }
.oth__spark-a { fill: var(--brand); fill-opacity: 0.1; }
.oth__spark-l {
  fill: none;
  stroke: var(--brand);
  stroke-width: 2;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.oth__kpi-s { font-size: var(--t-caption); color: var(--ink-3); }

/* ---- Your data: a setup log in days, against a project in weeks --------
   After the references' monospace training log. Drawn illustration, so the
   whole card is Geist Mono; the rows are the product's own before -> after
   grammar, + for added and ~ for changed to your definition. */
.odata {
  position: relative;
  width: 400px;
  margin: 60px 48px 0 0;      /* room for the project peeking out above-right */
}
.odata p,
.odata ol { margin: 0; }
/* The alternative: tilted the other way from the One thread ticket, so the
   two cells of the band mirror each other across their divider. */
.odata__proj {
  position: absolute;
  top: -76px;
  right: -64px;
  width: 256px;
  display: grid;
  gap: 6px;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  opacity: 0.8;
  transform: rotate(4deg);
  font-family: var(--font-product);
}
.odata__proj-h {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  color: var(--ink-faint);
}
.odata__proj-t {
  font-size: var(--t-caption);
  font-weight: 500;
  color: var(--ink-3);
}
/* 6 of 16 weeks. */
.odata__bar {
  display: block;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--bg-subtle);
  overflow: hidden;
}
.odata__bar span {
  display: block;
  width: 37.5%;
  height: 100%;
  background: var(--border-strong);
}

.odata__log {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--shadow-lg);
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  text-align: left;
  overflow: hidden;
}
.odata__head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--border);
  letter-spacing: var(--tr-mono);
  text-transform: uppercase;
  color: var(--ink-3);
  background: linear-gradient(to left, var(--approve-bg), var(--surface) 55%);
}
.odata__dots { display: inline-flex; gap: 4px; }
.odata__dots i {
  width: 8px;
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--border-strong);
}
.odata__live {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--approve);
}
.odata__live::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: var(--r-pill);
  background: currentColor;
}
.odata__rows {
  display: grid;
  gap: 10px;
  padding: var(--sp-4);
  list-style: none;
}
.odata__rows li {
  display: grid;
  grid-template-columns: 48px 16px minmax(0, 1fr);
  align-items: baseline;
  color: var(--ink-2);
  white-space: nowrap;
}
.odata__rows b { font-weight: 500; color: var(--ink); }
.odata__d { color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.odata__s { font-weight: 600; }
.odata__s--add { color: var(--approve); }
.odata__s--mod { color: var(--hold); }
.odata__s--ok { color: var(--approve); }
/* The row the whole log exists for. */
.odata__done {
  margin-top: 2px;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
  font-weight: 600;
}
.odata__done > span:last-child { color: var(--ink); }

/* ---- The move: the same three steps, two ways --------------------------
   SIMPLIFIED on 23 Sep: a detailed pass (exported charts, filter chips,
   faces, a mini flow, the full approval card) read as noise ("too crowdy to
   even tell whats happening"). What survives is the comparison itself. Both
   lanes run on ONE column template, label / step / join / step / join /
   step / join / outcome, so every step sits directly over its twin and the
   eye reads down, not along. Elsewhere: grey dashed cards, joins that cost
   days, ending still in review. aiRA: white cards on one brand line, ending
   held for approval. Below 720 of container width the lanes stand up side by
   side and run downward. */
.omove {
  /* The drawing is one 1120x380 coordinate space. The SVG carries it as a
     viewBox and every node is placed from the same numbers, converted to
     percentages by --x / --y, so the wires and the things riding them scale
     together and can never drift apart. Percentages rather than a transform
     zoom: the box keeps the ratio, so everything in it stays put. */
  --w: 1120;
  --h: 380;
  position: relative;
  width: min(1120px, 96%);
  aspect-ratio: 1120 / 380;
  text-align: left;
}
.omove p { margin: 0; }
/* The routes are grouping only: their children place themselves against
   .omove. The boxes exist so the narrow fallback has something to stack. */
.omove__route { display: contents; }

.omove__wires { position: absolute; inset: 0; width: 100%; height: 100%; }
/* The axis the question sits on, and the vertical that marks the middle of
   the run. --frame-grid, not --frame-line: the page frame's own distinction
   between structure and texture applies inside a drawing too. */
/* currentColor so the gradient in the markup has something to reference; the
   stroke itself is painted by that gradient. */
.omove__wires { color: var(--frame-grid); }
.omove__axis { stroke-width: 1; }
.omove__path { stroke-width: 2; stroke-linecap: round; }
.omove__path--aira { stroke: var(--brand); }
.omove__path--else { stroke: var(--ink-faint); stroke-dasharray: 3 6; }

/* ---- Nodes --------------------------------------------------------------
   A tile on the route with its label outside the curve, which is the
   reference's grammar: the mark rides the line and the word sits clear of
   it. The tile is opaque, so it masks the path running underneath instead of
   the path needing a gap cut in it. */
/* The node is a ZERO-SIZED ANCHOR at its coordinate, with the tile centred on
   it and the label hung outside. It cannot be a grid of tile-over-label with
   the pair centred: that puts the PAIR's middle on the curve, so every tile
   rides half a label's height above its own point and the marks sit off the
   line they are meant to be on. Cost an iteration; the tell is tiles that
   look consistently high. */
.omove__n {
  position: absolute;
  left: calc(var(--x) / var(--w) * 100%);
  top: calc(var(--y) / var(--h) * 100%);
  width: 0;
  height: 0;
  white-space: nowrap;
}
.omove__tile {
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border-radius: 14px;
  border: 1px solid var(--border);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  color: var(--ink-3);
}
.omove__lb {
  position: absolute;
  left: 50%;
  translate: -50% 0;
}
/* Clear of the tile's own half-height, so the gap is the same on both routes. */
.omove__route--aira .omove__lb { bottom: 36px; }
.omove__route--else .omove__lb { top: 36px; }
.omove__n--hub .omove__lb { top: 40px; }
.omove__tile svg { width: 20px; height: 20px; }
.omove__lb {
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: var(--tr-mono);
  text-transform: uppercase;
  color: var(--ink-3);
}
.omove__route--aira .omove__tile { color: var(--brand); border-color: var(--brand-line); }
.omove__route--aira .omove__lb   { color: var(--ink-2); }
/* Muted, NOT faint. The comparison only works if both routes are legible;
   the first pass dropped this one to --ink-faint on a dashed hairline and it
   disappeared, which reads as one route and a smudge. */
.omove__route--else .omove__tile {
  color: var(--ink-3);
  border-color: var(--border-strong);
  border-style: dashed;
  box-shadow: none;
  background: var(--bg-subtle);
}
.omove__route--else .omove__lb { color: var(--ink-3); }

.omove__n--hub { --x: 80; --y: 190; }
.omove__n--hub .omove__tile { width: 60px; height: 60px; border-radius: 16px; color: var(--ink-2); }
.omove__n--hub .omove__lb { color: var(--ink-3); }

.omove__n--a1 { --x: 330; --y: 100; }
.omove__n--a2 { --x: 540; --y:  88; }
.omove__n--a3 { --x: 750; --y: 100; }
.omove__n--e1 { --x: 330; --y: 280; }
.omove__n--e2 { --x: 540; --y: 292; }
.omove__n--e3 { --x: 750; --y: 280; }

/* ---- Route identity, sitting just past the fork ------------------------- */
.omove__id {
  position: absolute;
  left: calc(var(--x) / var(--w) * 100%);
  top: calc(var(--y) / var(--h) * 100%);
  translate: 0 -50%;
  display: grid;
  gap: 2px;
  white-space: nowrap;
}
.omove__route--aira .omove__id { --x: 176; --y: 78; }
.omove__route--else .omove__id { --x: 176; --y: 306; }
.omove__id b {
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: var(--tr-mono);
  text-transform: uppercase;
  color: var(--ink-3);
}
.omove__id span { font-size: var(--t-caption); color: var(--ink-faint); }
.omove__route--aira .omove__id span { color: var(--ink-2); }
.omove__brand { display: block; }
.omove__brand img { display: block; width: auto; height: 14px; }

/* ---- What each hand-off costs, set above the lower curve ---------------- */
/* ON the dashed line, not floating above it. Each hand-off is a stop on that
   route, and an opaque chip masks the line under it the way the tiles do, so
   the cost reads as part of the path rather than as an annotation near it. */
.omove__cost {
  position: absolute;
  left: calc(var(--x) / var(--w) * 100%);
  top: calc(var(--y) / var(--h) * 100%);
  translate: -50% -50%;
  padding: 3px 9px;
  border-radius: var(--r-pill);
  background: var(--surface);
  border: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: var(--t-caption);
  color: var(--hold);
  white-space: nowrap;
}
.omove__cost--1 { --x: 435; --y: 287; }
.omove__cost--2 { --x: 645; --y: 287; }
.omove__cost--3 { --x: 862; --y: 267; }

/* ---- Where each route ends ---------------------------------------------- */
.omove__out {
  position: absolute;
  left: calc(var(--x) / var(--w) * 100%);
  top: calc(var(--y) / var(--h) * 100%);
  translate: -50% -50%;
  display: grid;
  justify-items: center;
  gap: 6px;
  white-space: nowrap;
}
.omove__route--aira .omove__out { --x: 1022; --y: 132; }
.omove__route--else .omove__out { --x: 1022; --y: 252; }
.omove__out i {
  font-family: var(--font-mono);
  font-size: var(--t-caption);
  font-style: normal;
  color: var(--ink-faint);
}
.omove__out--else > span {
  padding: 5px 10px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-pill);
  font-size: var(--t-caption);
  color: var(--ink-3);
}

/* Below 1000 the labels start touching, so the type steps down and the tiles
   with it; the geometry is untouched, because it is one coordinate space. */
@container (max-width: 1000px) {
  .omove__tile { width: 44px; height: 44px; border-radius: 12px; }
  .omove__n--hub .omove__tile { width: 50px; height: 50px; }
  .omove__tile svg { width: 18px; height: 18px; }
  .omove__lb, .omove__id b { font-size: var(--t-caption); letter-spacing: 0.04em; }
}
/* Below 720 the run is too long for the box: 1120 of drawing in about 300 of
   well renders the labels at 4px. It becomes two stacked routes instead, the
   wires come off (a curve with nothing riding it is decoration), and the
   nodes go back into flow. The comparison survives, which is the only part
   that has to. */
@container (max-width: 720px) {
  .omove {
    width: 100%;
    aspect-ratio: auto;
    display: grid;
    /* The two routes stand up SIDE BY SIDE and run downward, not stacked:
       stacked they came to 714px against a 420px well and were clipped. Side
       by side each column is its own route and the twin steps still sit level
       across the pair, which is the comparison the whole cell is making. */
    grid-template-columns: 1fr 1fr;
    align-items: start;
    gap: var(--sp-4);
  }
  /* Specificity, not order: .omove__n below sets display:flex at the same
     weight, so a bare .omove__n--hub { display:none } loses to it and the hub
     stays in the grid, taking the first column and pushing the routes out of
     their pair. */
  .omove__wires,
  .omove .omove__n--hub { display: none; }
  .omove__route {
    display: grid;
    grid-auto-flow: row;
    gap: var(--sp-3);
    justify-items: start;
  }
  .omove__n, .omove__id, .omove__out, .omove__cost {
    position: static;
    translate: none;
    width: auto;
  }
  .omove__n {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    width: auto;
    height: auto;
  }
  .omove__tile, .omove__lb { position: static; translate: none; }
  .omove__tile { width: 36px; height: 36px; border-radius: 10px; }
  .omove__cost { justify-self: start; margin-left: 18px; }
  .omove__out { justify-items: start; }
  .omove__id { margin-bottom: var(--sp-2); }
  .omove__lb { font-size: var(--t-caption); }
}
@container (max-width: 440px) {
  .omove__m { display: none; }
  .omove__k span { display: none; }
}

/* ---- The method: a dark query with its two assumptions pinned ----------
   The one dark surface in the block. Two lines are marked and numbered, and
   the notes that answer them float off the panel's corner, after the
   references' alert card over a form. */
.ometh {
  position: relative;
  width: 360px;
  margin: 0 184px 48px 0;     /* room for the notes hanging off the corner */
  text-align: left;
}
.ometh p, .ometh ol { margin: 0; }
.ometh__code {
  border-radius: var(--r-md);
  background: var(--surface-inv);
  box-shadow: var(--shadow-lg);
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  overflow: hidden;
}
.ometh__tabs {
  display: flex;
  gap: var(--sp-5);
  padding: var(--sp-3) var(--sp-4) 0;
  border-bottom: 1px solid var(--ink-inv-3);
  color: var(--ink-inv-3);
}
.ometh__tabs span { padding-bottom: var(--sp-3); }
.ometh__tabs .is-on {
  color: var(--accent-on-dark);
  box-shadow: inset 0 -1px 0 var(--accent-on-dark);
}
.ometh__lines {
  display: grid;
  gap: 6px;
  padding: var(--sp-4);
  list-style: none;
  counter-reset: ln;
  color: var(--ink-inv-2);
  white-space: pre;
}
.ometh__lines li {
  position: relative;
  counter-increment: ln;
  padding-left: 28px;
}
.ometh__lines li::before {
  content: counter(ln);
  position: absolute;
  left: 0;
  color: var(--ink-inv-3);
}
.ometh__lines b { font-weight: 500; color: var(--primary-200); }
.ometh__lines i { font-style: normal; color: var(--accent-on-dark); }
.ometh__lines .is-mark { color: var(--ink-inv); }
.ometh__lines .is-mark::after,
.ometh__notes li::before {
  content: attr(data-n);
  display: inline-grid;
  place-items: center;
  width: 18px;
  height: 18px;
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  font-weight: 600;
  color: var(--surface-inv);
  background: var(--accent-on-dark);
}
.ometh__lines .is-mark::after { margin-left: var(--sp-2); vertical-align: middle; }
.ometh__notes {
  position: absolute;
  right: -208px;
  bottom: -48px;
  display: grid;
  gap: var(--sp-2);
  width: 232px;
  padding: 0;
  list-style: none;
}
.ometh__notes li {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr);
  column-gap: var(--sp-3);
  padding: var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--shadow-float);
  font-size: var(--t-caption);
  line-height: 1.45;
  color: var(--ink);
}
.ometh__notes li::before {
  grid-row: span 2;
  color: var(--on-brand);
  background: var(--brand);
}
.ometh__nk {
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  letter-spacing: var(--tr-mono);
  text-transform: uppercase;
  color: var(--ink-3);
}

/* ---- Points programme: one balance, in the programme's own words ------ */
.opts {
  width: min(520px, 86%);
  height: auto;
  overflow: visible;
}
.opts text {
  font-family: var(--font-sans);
  font-size: 12px;
  fill: var(--ink-2);
}
.opts .opts__cap,
.opts .opts__band,
.opts .opts__x {
  font-family: var(--font-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.opts .opts__cap { fill: var(--ink-3); }
.opts .opts__x { fill: var(--ink-faint); }
.opts .opts__band { fill: var(--brand-ink); }
.opts__base { stroke: var(--border-strong); }
.opts__lapse { fill: var(--wash-primary); }
.opts__lapse-e { stroke: var(--brand-line); stroke-dasharray: 3 4; }
.opts__area { fill: var(--brand); fill-opacity: 0.08; }
.opts__line {
  fill: none;
  stroke: var(--brand);
  stroke-width: 2;
  stroke-linejoin: round;
}
.opts__pt { fill: var(--surface); stroke: var(--brand); stroke-width: 2; }
.opts .opts__lab { font-weight: 550; fill: var(--ink); }
.opts__offer { fill: var(--brand); stroke: var(--surface); stroke-width: 3; }
.opts__offer-l { stroke: var(--brand); stroke-dasharray: 2 3; }
.opts__tag { fill: var(--surface); stroke: var(--brand-line); }
.opts .opts__tag-t { font-weight: 600; fill: var(--brand-ink); }
.opts__exp { fill: var(--hold); }
.opts .opts__exp-t { fill: var(--hold); font-weight: 600; }
.opts .opts__exp-n { font-weight: 450; }

/* The drawing is laid out at one size and scaled to its well, so the card's
   proportions and its 12px type survive together; laid out fluidly it would
   rewrap into a different picture at every width.
   ZOOM, not transform: scale. A transform shrinks the picture but not the
   box it occupies, so the centred card kept its full height and the well
   clipped its footer, which is the one row carrying the claim. Keyed to the
   well's HEIGHT, since that is the tight axis at 3/2. */
@container (max-height: 410px) { .oth, .odata, .ometh { zoom: 0.92; } }
@container (max-height: 370px) { .oth, .odata, .ometh { zoom: 0.84; } }
@container (max-height: 330px) { .oth, .odata, .ometh { zoom: 0.76; } }
@container (max-height: 290px) { .oth, .odata, .ometh { zoom: 0.66; } }
@container (max-height: 250px) { .oth, .odata, .ometh { zoom: 0.58; } }

/* ---- The wide cell -------------------------------------------------------
   CENTRED, which is what the reference does with its own full-width cell and
   is the reason the row reads as a band rather than as a stretched copy of
   the four around it. The measure is held well inside the rails, because
   centred copy across a 1240px container is unreadable whatever its size.
   -------------------------------------------------------------------------- */
.bento__cell--wide {
  grid-column: 1 / -1;
  text-align: center;
}
/* Its well is twice as wide as the others, so 3/1 is the ratio that makes it
   exactly as TALL as theirs: a narrow well is half the grid at 3/2, which is
   grid/3, and so is a full-width one at 3/1. Leaving it on 3/2 and letting
   max-height do the work only matched at the widths where both were capped,
   and everywhere below that the middle band sat taller than the two it
   separates. */
.bento__cell--wide .bento__fig { aspect-ratio: 3 / 1; }
.bento__cell--wide .bento__label { justify-content: center; }
.bento__cell--wide .bento__t,
.bento__cell--wide .bento__p {
  max-width: 62ch;
  margin-inline: auto;
}


/* ---- Getting started (rebuilt 22 Sep, rebuilt again 23 Sep) -------------
   FOUR CLICKABLE BOXES OVER ONE LINE GRAPH, from a tryplayground.com
   reference the user supplied: "you got the 4 phase blocks, clean them and
   make their boxes clickable, below them is a big line graph, start from 0,
   then based on click it increases."

   One deliberate departure from the reference. It marks the chosen column
   with a pale blue FILL; this site signals a selected item with colour on
   its label and its mark, never a background (22 Sep, user, on the
   accordion: "dont do bg fill on hover, rather subtle color", "the icon
   should stay gray if not active"). So the chosen phase takes a brand
   border, a brand phase number and a lit node, and the four boxes keep one
   surface between them.

   The rail this replaced is gone with it. It was one hairline with four
   brand squares on it, and it existed to carry the sequence; the graph does
   that now, and a rail above a curve is the same statement twice. What the
   rail got right is kept: all four nodes were lit at once, which left the
   current phase with no mark of its own, and the nodes are grey until
   chosen now.
   -------------------------------------------------------------------------- */

/* FOUR ROWS, one per part of a phase, and each box spans them and takes them
   back with subgrid: meta, title, description, output. That is what keeps the
   rule above the output landing at ONE height across the row.

   Two simpler things were tried and both fail the same way. margin-top:auto
   on the output, and flex:1 on the description, both FLOOR the output, which
   aligns the four outputs' bottoms; one of them runs to two lines and another
   to one, so their rules then sit 21px apart across boxes of equal height.
   The rule is at the TOP of that block, so the top is what has to align. */
/* With the tabs on the rails the section's real left margin is their 24px,
   not .container's 20, so the head takes the same giveback and pad the bento
   head does, or its text sits 4px off the tabs' and reads as a mistake. */
.rollout .section-head {
  margin-inline: calc(-1 * var(--sp-5));
  padding-inline: var(--sp-6);
}

.rollout__track {
  /* The positioning context for .rollout__tab, which is measured in px off
     each step's offset and so has to share the track's own origin. */
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-template-rows: repeat(4, auto);   /* meta, title, description, output */
  gap: 0;
  margin-top: var(--sp-12);
  /* Out to the rails, the same 20px giveback the bento uses, so the tabs'
     outer edges sit on the frame's verticals and their rules run rail to
     rail. Unwound below 992 with the rails. */
  margin-inline: calc(-1 * var(--sp-5));
  padding: 0;
  list-style: none;
  border: 1px solid var(--frame-line);
}
/* TABS, not cards (user, 23 Sep: "smaller tabs, no gap, more boxy shape and
   more connected to left right border"). No gap, no radius, no shadow, no
   lift: the four share hairlines and read as one control, and the board
   under them shares their bottom edge. The chosen tab is marked by a brand
   bar along its top, which is a tab's own grammar and is still not a
   background fill (22 Sep: "dont do bg fill on hover, rather subtle color"). */
.rollout__step {
  position: relative;
  display: grid;
  grid-row: span 4;
  grid-template-rows: subgrid;
  row-gap: 0;
  padding: var(--sp-5) var(--sp-6);
  background: var(--surface);
  cursor: pointer;
}
.rollout__step + .rollout__step { border-left: 1px solid var(--frame-line); }
/* ONE rule that SLIDES between the tabs (user, 23 Sep: "the top horizontal
   line should move and not regrow in each tab. same tab line to move
   across"). It was a bar per box, scaled up from zero on the chosen one and
   back down on the last: four marks taking turns, which reads as four
   separate events rather than as one thing moving.

   Its geometry comes from the chosen step's OWN offset within the track,
   measured by js/rollout.js, not from a column fraction. That is what lets
   the same element follow the 4-up, the 2x2 below 1099 and the stack below
   640 without the script knowing which of them is on, and it is why it
   carries a --tab-y as well: in a 2x2 the rule has to change rows.

   The transition is held until .is-live so the first placement lands rather
   than sliding in from the corner. */
.rollout__tab {
  position: absolute;
  top: -1px;
  left: 0;
  height: 2px;
  width: var(--tab-w, 25%);
  background: var(--brand);
  transform: translate(var(--tab-x, 0px), var(--tab-y, 0px));
  pointer-events: none;
}
.rollout.is-live .rollout__tab {
  transition: transform var(--dur-slow) var(--ease),
              width var(--dur-slow) var(--ease);
}
/* One row fewer once the output is out of flow: it is absolutely placed by
   .sr-only, so it stops claiming a track. */
.rollout.is-live .rollout__track { grid-template-rows: repeat(3, auto); }
.rollout.is-live .rollout__step { grid-row: span 3; }

/* The state mark: an empty ring for a phase still ahead, a filled ring for
   the chosen one, a check for the ones already behind it. So the row reads
   as progress, not as four equal options. js/rollout.js adds it. */
.rollout__mark {
  position: absolute;
  top: calc(var(--sp-5) + 2px);
  right: var(--sp-6);
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  color: var(--brand-ink);
}
.rollout__mark::before {
  content: '';
  box-sizing: border-box;
  width: 16px;
  height: 16px;
  border: 1.5px solid var(--border-strong);
  border-radius: var(--r-pill);
  transition: border-color var(--dur) var(--ease), border-width var(--dur) var(--ease);
}
.rollout__mark svg { display: none; width: 20px; height: 20px; }
.rollout__step:hover .rollout__mark::before,
.rollout__step.is-hover .rollout__mark::before { border-color: var(--brand-line); }
.rollout__step.is-on .rollout__mark::before { border: 5px solid var(--brand); }
.rollout__step.is-done .rollout__mark::before { display: none; }
.rollout__step.is-done .rollout__mark svg { display: block; }

/* The whole box is the target, and this is how it becomes one without the
   markup putting a <button> around an <h3>, which is invalid: the button
   wraps the TITLE, which gives it a sensible accessible name, and a
   pseudo-element stretches it over the box. js/rollout.js builds it. */
.rollout__btn {
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.rollout__btn::after {
  content: '';
  position: absolute;
  inset: 0;
}
.rollout__btn:focus-visible { outline: none; }
.rollout__btn:focus-visible::after {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

/* One item since 23 Sep, when the WHO chips came off (user: "also remove the
   needs you, capillary runs it, etc."). Nothing was lost: the section's own
   sub already says "your team is needed in the first and the third", so the
   chips were repeating a sentence four times, once per box. */
.rollout__meta {
  display: flex;
  align-items: center;
  margin-bottom: var(--sp-3);
  padding-right: var(--sp-8);   /* clears the state mark */
}
/* Grey until chosen: four lit numbers mark nothing. */
.rollout__n {
  font-family: var(--font-display);
  font-size: var(--t-caption);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
  transition: color var(--dur) var(--ease);
}
.rollout__step.is-on .rollout__n { color: var(--brand-ink); }

/* Who the phase needs. The two that want the customer's own people are the
   ones a buyer is scheduling around, so those carry colour; the two Capillary
   runs stay quiet. This is a LABEL and not a state, which is why it keeps a
   fill where the selection does not. */

.rollout__t {
  font-size: var(--t-title-sm);
  font-weight: 500;
  letter-spacing: var(--tr-title);
  line-height: var(--lh-title);
  color: var(--ink);
  margin-bottom: var(--sp-2);
  transition: color var(--dur) var(--ease);
}
.rollout__step:hover .rollout__t,
.rollout__step.is-hover .rollout__t { color: var(--brand-ink); }
.rollout__d {
  font-size: var(--t-body-sm);
  line-height: 1.65;
  color: var(--ink-3);
  margin-bottom: var(--sp-5);
}
/* With the output gone the paragraph closes the box, so the box's own bottom
   padding is the only gap wanted under it. */
.rollout.is-live .rollout__d { margin-bottom: 0; }

/* The output MOVED TO THE GRAPH on 23 Sep (user: "try to show the output on
   the graph, rather than on card"). This rule is what a reader sees with the
   script off; once it takes over, .rollout.is-live sends the block to
   .sr-only. HIDDEN RATHER THAN REMOVED: the pill on the graph is aria-hidden
   decoration, so display:none would drop four facts from the page as markup,
   and the section's own sub promises "four phases with a named output each". */
.rollout__out {
  padding-top: var(--sp-4);
  border-top: 1px solid var(--border);
  font-size: var(--t-body-sm);
  line-height: 1.5;
  color: var(--ink-2);
}
.rollout.is-live .rollout__out {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.rollout__out-k {
  display: block;
  margin-bottom: 2px;
  font-family: var(--font-display);
  font-size: var(--t-caption);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ---- The board: graph over parity gate -----------------------------------
   The chart and the gate are ONE object since 23 Sep. The gate is the
   condition the curve has to clear before go live, so it is the chart's own
   footer and it is drawn ON the chart as the line between phase three and
   phase four, rather than standing as a separate block under the section.

   The chart is four PHASE COLUMNS (js/rollout.js), each a click target with
   its number in an axis strip, so the graph says which phase is chosen and
   which are behind it without the reader looking back up at the boxes. The
   columns are equal on purpose: Capillary publishes two to four weeks in
   total and no per-phase split. There is NO scale on either axis: a y axis
   would be a measurement the site does not have.

   The ruled lines are pitched at 40px, which divides --cell (80px), so they
   register with the frame's own graph paper instead of cutting across it.
   -------------------------------------------------------------------------- */
/* Joined to the tabs and bled to the rails like them: the tabs' bottom rule
   is the chart's top edge, and nothing on the board is rounded. */
.rollout__board { margin-inline: calc(-1 * var(--sp-5)); }

.rollout__chart {
  --p: 1;
  --axis: 0px;
  position: relative;
  /* Not overflow:hidden: the marker sits ON the right edge at the last phase
     and should hang over it rather than be sliced in half. The <svg> clips
     itself to its own viewport. */
  padding-bottom: var(--axis);
  border-inline: 1px solid var(--frame-line);
  background: var(--surface);
}
.rollout__chart.is-live { --axis: 44px; }

.rollout__plot {
  position: relative;
  z-index: 1;
  height: 320px;
  /* The columns underneath take the clicks; everything drawn up here is
     decoration over them. */
  pointer-events: none;
  background-image: repeating-linear-gradient(to bottom,
    transparent 0 39px, var(--frame-grid) 39px 40px);
}
.rollout__svg {
  display: block;
  width: 100%;
  height: 100%;
}
/* Stops rather than raw hexes, so the fill stays on the token. */
.rollout__stop--hi { stop-color: var(--brand); stop-opacity: 0.18; }
.rollout__stop--lo { stop-color: var(--brand); stop-opacity: 0; }

/* The route ahead. Dashed, so it reads as pending. */
.rollout__ghost {
  fill: none;
  stroke: var(--border-strong);
  stroke-width: 1.5;
  stroke-dasharray: 2 6;
  stroke-linecap: round;
}
.rollout__area { fill: url(#rollout-fill); }
/* A wide, faint stroke under the line, so the travelled route has some
   weight against the dashed one ahead of it. */
.rollout__halo {
  fill: none;
  stroke: var(--brand);
  stroke-opacity: 0.12;
  stroke-width: 8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.rollout__line {
  fill: none;
  stroke: var(--brand);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The curve is clipped from the RIGHT back to --p. Percentages in a clip-path
   on an SVG element resolve against its own fill box, and every path spans
   the full width, so they clip identically and the fill never runs ahead of
   the line. The transition is only live once the script has taken over, or
   the collapse to zero on load would animate in reverse. */
.rollout__area,
.rollout__halo,
.rollout__line { clip-path: inset(0 calc((1 - var(--p)) * 100%) 0 0); }
.rollout__chart.is-live .rollout__area,
.rollout__chart.is-live .rollout__halo,
.rollout__chart.is-live .rollout__line {
  transition: clip-path var(--dur-slow) var(--ease);
}

/* ---- Phase columns and the axis strip ---- */
.rollout__cols {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
}
.rollout__col {
  position: relative;
  border-left: 1px solid var(--border);
  cursor: pointer;
}
.rollout__col:first-child { border-left: 0; }
/* The parity gate is the boundary into go live: dashed while it is ahead,
   brand once the chosen phase has reached it. */
.rollout__col--gate { border-left: 1px dashed var(--border-strong); }
.rollout__chart.is-gated .rollout__col--gate { border-left-color: var(--brand); }

.rollout__col-n {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--axis);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  border-top: 1px solid var(--border);
  font-family: var(--font-display);
  font-size: var(--t-caption);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
  transition: color var(--dur) var(--ease);
}
/* The frame's own crossing mark, grey until its phase is reached. */
.rollout__col-n::before {
  content: '';
  width: 6px;
  height: 6px;
  background: var(--border-strong);
  transition: background var(--dur) var(--ease);
}
.rollout__col.is-done .rollout__col-n { color: var(--ink-2); }
.rollout__col.is-done .rollout__col-n::before { background: var(--ink-3); }
.rollout__col.is-on .rollout__col-n,
.rollout__col.is-hover .rollout__col-n { color: var(--brand-ink); }
.rollout__col.is-on .rollout__col-n::before { background: var(--brand); }

/* ---- What rides the curve ----
   HTML, not SVG. The chart is drawn with preserveAspectRatio="none" so it
   fills any box, which means a <circle> inside it would render as an ellipse
   at every width but one. */
.rollout__dot,
.rollout__drop,
.rollout__node,
.rollout__gate-tag,
.rollout__pill { display: none; }
.rollout__chart.is-live .rollout__dot,
.rollout__chart.is-live .rollout__drop,
.rollout__chart.is-live .rollout__node,
.rollout__chart.is-live .rollout__pill { display: block; }
.rollout__chart.is-live .rollout__gate-tag { display: inline-flex; }

/* A dashed drop from the marker to the floor of the plot, so the chosen
   point reads against its column rather than floating in it. */
.rollout__drop {
  position: absolute;
  left: var(--px, 0%);
  top: var(--py, 100%);
  bottom: 0;
  width: 0;
  border-left: 1px dashed var(--brand);
  opacity: 0.45;
  transition: left var(--dur-slow) var(--ease), top var(--dur-slow) var(--ease);
}

/* The other three phases' points: filled once behind the chosen one, hollow
   ahead of it, and ringed when their box or column is hovered. */
.rollout__node {
  position: absolute;
  width: 10px;
  height: 10px;
  margin: -5px 0 0 -5px;
  border-radius: var(--r-pill);
  background: var(--surface);
  box-shadow: inset 0 0 0 1.5px var(--border-strong);
  transition: background var(--dur) var(--ease), box-shadow var(--dur) var(--ease), opacity var(--dur) var(--ease);
}
.rollout__node.is-done {
  background: var(--brand);
  box-shadow: 0 0 0 3px var(--surface);
}
.rollout__node.is-hover { box-shadow: 0 0 0 4px var(--brand-subtle), inset 0 0 0 1.5px var(--brand); }
.rollout__node.is-on { opacity: 0; }

.rollout__dot {
  position: absolute;
  z-index: 1;
  left: var(--px, 0%);
  top: var(--py, 100%);
  width: 16px;
  height: 16px;
  margin: -8px 0 0 -8px;
  border: 3px solid var(--surface);
  border-radius: var(--r-pill);
  background: var(--brand);
  box-shadow: 0 0 0 1px var(--brand-line), var(--shadow-sm);
  transition: left var(--dur-slow) var(--ease), top var(--dur-slow) var(--ease);
}
/* A slow ring off the marker: the one moving thing on a still chart, and the
   answer to "where am I" before anything is read. */
.rollout__dot::after {
  content: '';
  position: absolute;
  inset: -3px;
  border: 2px solid var(--brand);
  border-radius: var(--r-pill);
  opacity: 0;
  animation: rollout-pulse 2.4s var(--ease) infinite;
}
@keyframes rollout-pulse {
  0%   { transform: scale(1);   opacity: 0.5; }
  100% { transform: scale(2.4); opacity: 0; }
}

/* The gate on the chart, sat at the foot of its line. */
.rollout__gate-tag {
  position: absolute;
  z-index: 1;
  left: 75%;
  bottom: var(--sp-3);
  transform: translateX(-50%);
  align-items: center;
  gap: 6px;
  padding: 4px 12px 4px 8px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: var(--surface);
  box-shadow: var(--shadow-xs);
  font-size: var(--t-caption);
  font-weight: 600;
  line-height: 20px;
  white-space: nowrap;
  color: var(--ink-3);
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.rollout__gate-tag svg { width: 20px; height: 20px; flex: none; }
.rollout__chart.is-gated .rollout__gate-tag {
  border-color: var(--brand-line);
  color: var(--brand-ink);
}

/* Two lines, a label and the output, so it is a chart annotation rather than
   a pill: the site's own surface with a hairline and a float shadow. */
.rollout__pill {
  position: absolute;
  z-index: 2;
  left: var(--px, 0%);
  top: var(--py, 100%);
  transform: translate(var(--pull, -50%), var(--push, calc(-100% - 18px)));
  /* Sized by its text, not by what is left of the box. An absolutely placed
     element with left:100% and no right has ZERO available width, so at the
     last phase it shrink-wrapped to its longest word and stacked four lines
     tall. */
  width: max-content;
  max-width: 24rem;
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  /* The 2px brand rule is on the TOP, and it is the SAME MARK the chosen
     phase box wears (.rollout__step.is-on::before). Same white ground, same
     hairline, same bar: the annotation is that box appearing again at its
     point on the curve, rather than a second thing that happens to be near
     it. It was a brand bar down the LEFT with the radius zeroed, which is the
     stock callout every template ships and reads as one (user, 23 Sep: "looks
     very AI made with left border").
     No radius, because nothing else in this section has one: the boxes and
     the chart are drawn in hairlines. The shadow is the small one and not
     --shadow-float, which is the nav's; this only has to lift off a grid. */
  border: 1px solid var(--border);
  border-top: 2px solid var(--brand);
  border-radius: 0;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  transition: left var(--dur-slow) var(--ease), top var(--dur-slow) var(--ease);
}
.rollout__pill-k {
  display: block;
  margin-bottom: 2px;
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: var(--tr-mono);
  text-transform: uppercase;
  color: var(--brand-ink);
}
.rollout__pill-v {
  display: block;
  font-size: var(--t-body-sm);
  font-weight: 550;
  line-height: 1.45;
  color: var(--ink);
}

@media (prefers-reduced-motion: reduce) {
  .rollout__chart.is-live .rollout__area,
  .rollout__chart.is-live .rollout__halo,
  .rollout__chart.is-live .rollout__line,
  .rollout__dot,
  .rollout__drop,
  .rollout__node,
  .rollout__pill,
  .rollout.is-live .rollout__tab,
  .rollout__gate-tag,
  .rollout__col-n,
  .rollout__col-n::before,
  .rollout__step::before,
  .rollout__mark::before,
  .rollout__n,
  .rollout__t { transition: none; }
  .rollout__dot::after { animation: none; }
}

/* ---- The parity gate, as the board's footer ----------------------------
   Still the one loud block in the section: a brand-tinted ground, a labelled
   mark and the figure at display size. It sits UNDER the chart now, sharing
   its border, because it is the chart's condition: the line marked on the
   graph between phase three and four is this.
   -------------------------------------------------------------------------- */
.rollout__gate {
  position: relative;
  display: grid;
  /* ONE ROW since 23 Sep (user: "this can be made a bit compact"): label,
     figure, claim and capture side by side, the figure down a step to the
     H2 size and the claim to body. It was two rows with a 60px figure, which
     made the footer nearly as tall as the chart's own axis-to-curve span. */
  --gate-pad: var(--sp-4);
  grid-template-columns: auto auto minmax(0, 1fr) clamp(120px, 11%, 150px);
  grid-template-areas: 'label fig body shot';
  align-items: center;
  column-gap: var(--sp-5);
  padding: var(--gate-pad) var(--sp-6);
  border: 1px solid var(--frame-line);
  border-top-color: var(--border);
  background: var(--wash-primary);
  overflow: hidden;
}
/* The capture bleeds to the card's own edges, so it gets the padding back on
   three sides. It is a COLUMN and not an absolutely placed layer: the body
   copy has to be laid out around it, and a layer would let the sentence run
   underneath at the widths where it is longest. */
.rollout__gate-shot {
  grid-area: shot;
  position: relative;
  align-self: stretch;
  margin: calc(-1 * var(--gate-pad)) calc(-1 * var(--sp-6))
          calc(-1 * var(--gate-pad)) 0;
  overflow: hidden;
}
.rollout__gate-shot img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* The dashboard side of the capture, which is the half carrying numbers.
     Cropping in CSS rather than shipping a second file: it is the same asset
     the hero loads, so it costs no extra request. */
  object-fit: cover;
  object-position: 100% 30%;
  opacity: 0.5;
  /* Dissolved into the card rather than cut off at a hard edge, the same way
     the hero's glyph field and the category pages' mark row are handled. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 62%);
  mask-image: linear-gradient(to right, transparent 0, #000 62%);
}
.rollout__gate-k {
  grid-area: label;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: var(--tr-mono);
  text-transform: uppercase;
  color: var(--brand-ink);
}
.rollout__gate-k svg { width: 20px; height: 20px; flex: none; }

.rollout__gate-fig {
  grid-area: fig;
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin: 0;
  padding-left: var(--sp-5);
  border-left: 1px solid var(--brand-line);
}
/* At the H3 size since 23 Sep (user: "by compact i mean smaller"): the
   footer is a slim strip under the chart now, not a second headline. Sans
   with tabular numerals, never the mono, which gives a comma its own cell. */
.rollout__gate-n {
  font-family: var(--font-display);
  font-size: var(--t-display-3);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--tr-display);
  font-variant-numeric: tabular-nums;
  color: var(--brand-ink);
}
.rollout__gate-u {
  font-size: var(--t-caption);
  color: var(--ink-2);
  white-space: nowrap;
}
.rollout__gate-d {
  grid-area: body;
  font-size: var(--t-body);
  line-height: 1.55;
  color: var(--ink-2);
  margin: 0;
  max-width: 88ch;
  padding-left: var(--sp-5);
  border-left: 1px solid var(--brand-line);
}

/* Two across from 1099, not 1299. The switch was that high because the
   section wore .section--band, whose 136px inset squeezed the four boxes to
   178px tracks at a 1200 viewport; the band came off on 23 Sep (user: "remove
   the left right dotted padding border") and they have the whole container
   now, so four hold down to about 1100.

   It went VERTICAL here until 23 Sep, on the reasoning that four phases in
   two rows stop reading as a sequence. That reasoning is spent: the graph
   below them now carries the sequence, and each box says which phase it is,
   so a 2x2 costs the section nothing and saves a phone about 400px of
   scrolling past boxes it has already read. */
@media (max-width: 1099px) {
  /* Two bands of four rows. */
  .rollout__track {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: repeat(8, auto);
  }
  .rollout.is-live .rollout__track { grid-template-rows: repeat(6, auto); }
  .rollout__step:nth-child(odd) { border-left: 0; }
  .rollout__step:nth-child(-n+2) { border-bottom: 1px solid var(--frame-line); }
  .rollout__plot { height: 280px; }
  /* The capture goes and the label rides above figure and claim, so the
     claim keeps a readable measure in two-thirds of the width. */
  .rollout__gate-shot { display: none; }
  .rollout__gate {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: 'label label' 'fig body';
    row-gap: var(--sp-3);
  }
  .rollout__gate-fig { padding-left: 0; border-left: 0; }
}
@media (max-width: 640px) {
  /* One column, so there is nothing left to align across and the subgrid
     comes off: each box simply rows itself. */
  .rollout__track {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
  }
  .rollout__step,
  .rollout.is-live .rollout__step {
    grid-row: auto;
    grid-template-rows: none;
    display: flex;
    flex-direction: column;
  }
  .rollout.is-live .rollout__track { grid-template-rows: none; }
  .rollout__step + .rollout__step { border-left: 0; }
  .rollout__step:not(:last-child) { border-bottom: 1px solid var(--frame-line); }
  .rollout__plot { height: 240px; }
  /* A quarter of a phone is about 85px, so the axis keeps the number only. */
  .rollout__col-w { display: none; }
  /* The pill is most of the chart's width at this size, so it wraps and is
     held to a measure; js/rollout.js stops centring it on the marker below
     520px and anchors it to whichever side has the room. */
  /* It stops pointing at the marker here and becomes the chart's CAPTION,
     pinned to the top-left. At this width the annotation is most of the box,
     so there is no anchoring that keeps it inside: left-anchored it runs off
     the right at the middle phases, right-anchored it runs off the left, and
     centred it does both. The marker still moves; this just stops pretending
     to be a tooltip in a box too small for one. */
  .rollout__pill {
    left: var(--sp-3);
    top: var(--sp-3);
    transform: none;
    padding: var(--sp-2) var(--sp-3);
    max-width: calc(100% - 2 * var(--sp-3));
  }
}
@media (max-width: 640px) {
  /* One column, and the capture goes: a 200px slice of a screenshot beside
     stacked copy is noise rather than texture. */
  .rollout__gate-shot { display: none; }
  .rollout__gate {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: 'label' 'fig' 'body';
    row-gap: var(--sp-3);
    padding: var(--sp-5);
  }
  .rollout__gate-d {
    padding-left: 0;
    padding-top: var(--sp-3);
    border-left: 0;
    border-top: 1px solid var(--brand-line);
  }
  .rollout__gate-u { margin-top: 0; }
}


/* ---- Responsive ----------------------------------------------------------
   LAST IN THE FILE, and it has to stay last. Media queries add no specificity,
   so an override here only beats a base rule by coming after it. This block
   used to sit in the middle, which was invisible for a while because a
   duplicated copy of the file tail happened to repeat it below the sections it
   overrides; when that duplicate was removed on 22 Sep the page started
   overflowing on a phone. Add new section styles ABOVE this comment, never
   below it.
   -------------------------------------------------------------------------- */

@media (max-width: 991px) {
  .phero__fig { margin-top: var(--sp-12); }
  /* No glyph field below the rails, the same call the home hero's lattice
     takes: the copy has the container to itself here, so the clear ellipse
     would be sitting behind the headline rather than around it. The script
     reads a zero box, drops its grid and kills its timer, so nothing keeps
     ticking behind a hidden canvas. */
  .phero__matrix { display: none; }

  .feat__mini { grid-template-columns: 1fr 1fr; }

  /* One column, and the grid gives back its rail bleed: the rails come off at
     this width and .container drops the 20px gutter with them, so a negative
     margin here would hang the grid outside .padding-global. */
  .bento__grid { grid-template-columns: minmax(0, 1fr); margin-inline: 0; }
  /* Same giveback, same reason, for /product's rollout tabs and board. */
  .rollout .section-head,
  .rollout__track,
  .rollout__board { margin-inline: 0; }
  .rollout .section-head { padding-inline: 0; }
  .bento__head { margin-inline: 0; padding-inline: 0; max-width: 46rem; }
  /* Stacked, every cell closes its own row and only the last closes the grid,
     so the 2 / 1 / 2 edge rules above no longer describe anything. The side
     padding goes with them: there are no vertical lines left to sit in from,
     so it was only outdenting the section head against the cells and taking
     48px off a phone's measure. The block becomes a ruled list, which is the
     same shape .biz uses. */
  .bento__cell {
    border-right: 0;
    border-bottom: 1px solid var(--frame-line);
    padding-inline: 0;
    /* One column, so the six-row band structure has nothing left to align
       and each cell simply rows itself. */
    grid-row: auto;
    grid-template-rows: auto auto;
  }
  .bento__cell:last-child { border-bottom: 0; }
  /* The cell has no side padding here, so the well has nothing to bleed out
     of, and its inset narrows with the column. */
  /* One column: the cell has no side padding left, so there is nothing to
     bleed out of, and the 80px inset would sit a quarter of the way in. The
     ratio and the cap carry over unchanged. */
  .bento__fig {
    --fig-x: 40px;
    --fig-bleed: 0px;
  }
  /* Centring one cell in a single column reads as a mistake rather than as a
     band, because there is no row for it to be the width of. */
  .bento__cell--wide { text-align: left; }
  .bento__cell--wide .bento__label { justify-content: flex-start; }
  .bento__cell--wide .bento__t,
  .bento__cell--wide .bento__p { margin-inline: 0; max-width: 48ch; }
  /* One column, so it is no wider than the rest and takes their ratio back. */
  .bento__cell--wide .bento__fig { aspect-ratio: 3 / 2; }
  /* The move stands its two routes up and runs them downward here, so the well
     takes the drawing's own height instead of a ratio: capped at 1/1 and 420px
     it clipped the lower route. Safe below 992, where the bento's subgrid has
     already unwound and no neighbouring well has to match this one. */
  .bento__cell--wide .bento__fig--diagram {
    /* container-type must drop from size to INLINE-SIZE with the ratio. Size
       containment means the element is NOT sized by its contents, so a well
       that has given up its aspect-ratio collapses to 0 and clips the whole
       drawing. Inline-size contains only the width, which is all .omove's own
       container queries read anyway. */
    container-type: inline-size;
    aspect-ratio: auto;
    max-height: none;
    min-height: 0;
  }
  .bento__head { margin-bottom: var(--sp-10); }
}
@media (max-width: 991px) {
  /* The cert row gives back its rail bleed: the rails come off here and
     .container drops the gutter with them, so a negative margin would hang
     the row outside .padding-global. */
  .sec__certs { margin-inline: 0; }
  /* Six across becomes three by two. The marks are the content, so they keep
     their size and the cell simply gets shorter. */
  /* Already three across; only the cell height comes down. */
  .cert { min-height: 116px; }
}
@media (max-width: 640px) {
  /* Three marks stay on one row on a phone: at 390 that is a 116px track
     each, which still carries an 84px mark. Stacking them would cost 230px
     for no gain. */
  .cert { min-height: 96px; padding-inline: var(--sp-2); gap: var(--sp-2); }
  .cert__logo { height: 56px; }
  .cert:last-child .cert__logo { height: 52px; }
  /* "SOC 2 TYPE II" does not fit a 117px track on one line, and a wrapped
     mono label reads as two labels, so the row drops to the caption size and
     loses the tracking that was pushing it over. */
  .cert__name { font-size: var(--t-caption); letter-spacing: 0.04em; }
}
@media (max-width: 640px) {
  .phero__cta { margin-top: var(--sp-6); }
  /* Balance is ignored past six lines, and at this width the sub runs to
     seven and ended on a 14px orphan ("it."). `pretty` has no line limit and
     its whole job is the last line, so it pulls a word down instead. Firefox
     does not support it yet and simply wraps, which is what it did before. */
  .phero__sub { text-wrap: pretty; }
  .phero__fig { margin-top: var(--sp-10); }
  /* A phone cannot read a two-pane app scaled to 350px either way, so the
     frame gets taller and holds the THREAD half, which is the readable one
     and the one the page is about. */
  .phero__fig { aspect-ratio: 5 / 4; }
  .phero__fig > img,
  .phero__fig > iframe { object-position: top left; }


  .feat__mini { grid-template-columns: 1fr; }
}