/* Adjoltz suite page, phases 2 and 3.
   Sections 1 (hero) and 5 (the nine services) are ported from
   drafts/draft-handover.html, same class names, so that draft stays the readable reference.
   Section 7 (the thread) is ported from drafts/draft-answers.html the same way.
   Section 0 (the header mega menu), section 4 (the trust row) and section 6 (the four
   agents) are new work. Section 3 (the logo marquee) reuses .logos and .marquee from
   site.css and needs no rules here. Sections 8 and 9, the journey and the orbit of eighteen,
   are new work too, added after the thread. */

/* ---------- 0. header, the Platform mega menu ---------- */
/* the trigger is a button that has to read as one of the nav links beside it */
.world-a .suite-nav .has-mega { display: flex; align-items: center; }
.world-a .suite-nav .nav-trigger {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 10px 12px; border-radius: var(--r-full);
  font-size: var(--fs-sm); font-weight: 500; line-height: 1.45;
  color: inherit; white-space: nowrap;
}
.world-a .suite-nav .nav-trigger:hover,
.world-a .suite-nav .has-mega.is-open .nav-trigger { background: var(--lg); }
.world-a .suite-nav .nav-trigger .chev { width: 16px; height: 16px; flex: none; fill: currentColor; transition: transform .25s var(--ease); }
.world-a .suite-nav .has-mega.is-open .nav-trigger .chev { transform: rotate(180deg); }

/* the panel drops the full width of the header, closed state lives in CSS so it is
   never focusable and never in the accessibility tree while it is shut */
.world-a .suite-nav .mega {
  position: absolute; top: 100%; left: 0; right: 0;
  background: var(--white); color: var(--dg);
  border-top: 1px solid var(--gray1);
  box-shadow: var(--shadow-float);
  opacity: 0; visibility: hidden; transform: translateY(-8px);
  transition: opacity .22s var(--ease), transform .22s var(--ease), visibility 0s .22s;
}
.world-a .suite-nav .has-mega.is-open .mega {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity .22s var(--ease), transform .22s var(--ease), visibility 0s;
}
.world-a .suite-nav .mega-inner {
  max-width: var(--max); margin-inline: auto; padding: 30px var(--pad) 34px;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px 28px;
}
.world-a .suite-nav .mega-col { display: grid; align-content: start; gap: 2px; }
.world-a .suite-nav .mega-h { font-size: var(--fs-micro); font-weight: 500; text-transform: uppercase; letter-spacing: .04em; color: var(--gray2); margin-bottom: 10px; }
/* a column can hold a second group, so a heading that follows an item opens a gap the first
   heading in the column does not need */
.world-a .suite-nav .mega-item + .mega-h { margin-top: 22px; }
.world-a .suite-nav .mega-item {
  display: grid; grid-template-columns: 22px minmax(0, 1fr); column-gap: 12px; row-gap: 1px;
  padding: 7px 10px; border-radius: var(--r-sm); color: var(--dg);
  transition: background-color .18s var(--ease);
}
.world-a .suite-nav .mega-item:hover { background: var(--lg); text-decoration: none; }
.world-a .suite-nav .mega-item .ico { grid-column: 1; grid-row: 1 / span 2; width: 22px; height: 22px; display: grid; place-items: center; color: var(--dg); }
.world-a .suite-nav .mega-item .ico svg { width: 18px; height: 18px; fill: currentColor; }
.world-a .suite-nav .mega-item b { grid-column: 2; grid-row: 1; font-size: var(--fs-xs); font-weight: 500; line-height: 1.3; }
.world-a .suite-nav .mega-item span:not(.ico) { grid-column: 2; grid-row: 2; font-size: var(--fs-micro); color: var(--gray2); line-height: 1.3; }
.world-a .suite-nav .mega-aside { border-left: 1px solid var(--gray1); padding-left: 28px; }
.world-a .suite-nav .mega-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.world-a .suite-nav .mega-chips a { font-size: var(--fs-micro); line-height: 1.2; color: var(--dg); background: var(--lg); border-radius: var(--r-full); padding: 5px 10px; }
.world-a .suite-nav .mega-chips a:hover { background: var(--lg2); text-decoration: none; }
.world-a .suite-nav .mega-note { font-size: var(--fs-micro); color: var(--gray2); line-height: 1.35; margin-top: 14px; }

/* the nav on the page's centre line, not on the middle of what is left over.
   site.css lays the header out as a flex row and centres the links inside it with
   margin-inline: auto, which measures the gap between the logo and the actions rather than
   the page. The two ends do not weigh the same, the actions carry a button the logo does
   not, so that gap is off centre and the links sit left of the middle with it. Three columns
   with matched outer tracks move the measurement off the leftovers and on to the wrap: the
   logo takes the first track, the links the second, the actions the third, and because both
   outer tracks are one fraction each the middle one is centred however much either end
   grows. minmax(max-content, 1fr) is the guard on that, so an end that outgrows its fraction
   takes the width it needs instead of wrapping.
   Held above 1040 on purpose. Below it site.css hides the links and the header is the logo
   and the burger, two items, which a three column grid would put the burger in the middle
   of; at those widths site.css's flex row is untouched and still right. */
@media (min-width: 1041px) {
  .world-a .suite-nav .wrap {
    display: grid;
    grid-template-columns: minmax(max-content, 1fr) auto minmax(max-content, 1fr);
    align-items: center;
  }
  .world-a .suite-nav .logo { justify-self: start; }
  .world-a .suite-nav .nav-links { margin-inline: 0; justify-self: center; }
  .world-a .suite-nav .nav-actions { justify-self: end; }
}

/* ---------- 1. hero ---------- */
/* Every section below takes its ground from a token the page's world block sets, the way
   site.css's own sections read --feat-bg, --why-bg and --proof-bg. Nothing here picks a
   colour of its own, so the page's rhythm is legible in one place, in the world block. */
/* The hero is version A's, ported whole, so its rules live in the page's world block beside
   the rest of world A's own look and there is nothing for this file to add. The old plain
   hero's .hov-hero rules are gone with it. */

/* ---------- 1b. hero tabs, three standalone pills ----------
   These three used to be one control, a dark pill sliding inside a shared mint track.
   They are becoming three separate offers, so the track goes: each tab gets its own
   ground, its own border and a real gap from its neighbours, and reads as a button
   rather than a segment. .tabs and .tab still carry every size, weight and transition
   site.css gives them, plus the selected tab's --dg fill and --white label, unchanged.
   This block only undoes the shared track and adds what a standalone pill needs.
   Scoped to .suite-tabs, the class only this page's tab group carries alongside .tabs,
   so version A's tabs, built from the same two classes inside the same .hero, read
   exactly as they did before. */
.suite-tabs {
  background: transparent;
  border: 0;
  padding: 0;
  gap: 12px;
  flex-wrap: wrap;
  justify-content: center;
}
.suite-tabs .tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  min-height: 44px;
  white-space: nowrap;
  background: var(--white);
  border: 1px solid var(--gray1);
}
/* the hover only ever touches an unselected pill: the selected one keeps reading dg,
   the same whether the pointer is over it or not, same as it reads today */
.suite-tabs .tab:hover:not([aria-selected="true"]) {
  background: var(--lg);
  border-color: var(--dg);
  transform: translateY(-1px);
}
/* .suite-tabs .tab and site.css's own .tab[aria-selected="true"] tie in specificity, and
   this file loads second, so its white ground would otherwise win the tie and paint the
   selected pill's white label onto its own white fill. Restated here, at a selector this
   page's stylesheet cannot lose to, on the same --dg and --white site.css already uses,
   plus the border colour that matches the fill so it reads as one solid pill rather than
   a pale ring around a dark one. */
.suite-tabs .tab[aria-selected="true"] {
  background: var(--dg);
  color: var(--white);
  border-color: var(--dg);
}
/* the ring used to land on the shared track; now it has to hug each pill on its own */
.suite-tabs .tab:focus-visible {
  border-radius: var(--r-full);
}

@media (prefers-reduced-motion: reduce) {
  .suite-tabs .tab:hover { transform: none; }
}

/* ---------- 4. the trust row (new) ---------- */
/* the first change of ground on the page. The logo strip above it already carries the rule
   that closes the white run, so the cream starts straight under that line. */
.world-a .fact-strip { background: var(--strip-bg); }
.world-a .fact-row { display: grid; grid-template-columns: repeat(3, 1fr); }
.world-a .fact { padding: 4px clamp(20px, 3vw, 40px); border-left: 1px solid var(--gray1); text-align: center; display: grid; gap: 6px; }
.world-a .fact:first-child { border-left: 0; }
.world-a .fact-name { font-size: var(--fs-md); font-weight: 500; color: var(--dg); }
.world-a .fact-line { font-size: var(--fs-sm); color: var(--gray2); margin: 0; }

/* ---------- 5. the nine services ---------- */
/* The ground and the closing line. The nine themselves, and the screen they point at, are
   laid out in 14c: they are one figure now rather than a mockup above a ruled list. */
.world-a .hov-desk { background: var(--desk-bg); }
/* the closing line is centred on the page, not inside a column, so it drops the 62ch cap */
.world-a .hov-close { max-width: none; text-align: center; font-size: var(--fs-lg); color: var(--dg); margin-top: clamp(32px, 4vw, 48px); }

/* ---------- 6. the four agents (new) ---------- */
/* The one dark block on the page, and the page's spine: it does for this page what the dark
   features block does for version A. It reuses site.css's .feat card at the --feat tokens the
   world block already holds, so nothing here picks a colour. site.css keys the dark block's
   head and copy off .features, which this section is not, so those two rules are repeated
   below against the same tokens rather than duplicated with values of their own. */
.world-a .agents { background: var(--feat-bg); color: var(--feat-fg); }
.world-a .agents .sec-head h2 { color: var(--feat-h); }
.world-a .agents .sec-head p { color: var(--feat-muted); }
.world-a .agents .feat-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.world-a .agents .feat { min-height: 0; grid-template-rows: auto auto; align-content: start; gap: 16px; padding: 26px; }
/* the plate under the sprite. The glyph is currentColor, so it follows the plate's own
   foreground rather than the card's, and both are assigned in the world block. */
.world-a .agents .feat .mark { width: 40px; height: 40px; border-radius: 12px; background: var(--feat-mark-bg); color: var(--feat-mark-fg); display: grid; place-items: center; }
.world-a .agents .feat .mark svg { width: 22px; height: 22px; fill: currentColor; }
.world-a .agents .feat h3 { font-size: var(--fs-h6); margin-bottom: 6px; }
/* was the light world's --gray2, which is unreadable on this ground */
.world-a .agents-close { max-width: none; text-align: center; font-size: var(--fs-md); color: var(--feat-muted); margin-top: clamp(32px, 4vw, 48px); }

/* ---------- 7. the thread, ported from drafts/draft-answers.html ---------- */
/* back to white under the dark block, which is the whole point of the dark block */
.world-a #answers { background: var(--answers-bg); }

/* Layout. Two columns from 900 up: the section head and the questions take the left 42%,
   the thread takes the right 52%, and the 6% between them is the whole of the remainder, so
   both columns measure exactly what they are described as at every width. Below 900 it is
   one column and the thread climbs above the list, because a question pressed at the bottom
   of a phone screen has to put its answer above the thumb rather than off the top.
   The single column is the default and the two are the media query, so the narrow layout is
   the one that survives if this rule is the last thing a browser understands. */
.answers {
  display: grid;
  grid-template-columns: minmax(0, 620px);
  justify-content: center;
  row-gap: clamp(22px, 2vw, 28px);
  align-items: start;
}

/* the head of the section, in the left column with the list rather than centred over both */
.hero-col { grid-column: 1; grid-row: 1; display: grid; justify-items: start; gap: 14px; text-align: left; }
.hero-col h2 { line-height: 1.04; max-width: 14ch; color: var(--dg); overflow-wrap: break-word; }
.hero-col .lead { font-size: var(--fs-md); line-height: 1.45; color: var(--gray2); max-width: 46ch; }

/* The questions, as a plain FAQ list: a question, a rule, a question, a rule. Each row is a
   button the full width of the column with the mark at its right edge, and the row carries
   the proof line the question earns, so there is one place to look rather than two. */
.faq-list { grid-column: 1; grid-row: 3; list-style: none; border-top: 1px solid var(--gray1); }
.faq-row { border-bottom: 1px solid var(--gray1); }
.faq-q {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 22px;
  column-gap: 16px;
  align-items: center;
  width: 100%;
  min-height: 56px;
  padding-block: 15px;
  text-align: left;
  color: var(--dg);
}
.faq-text { grid-column: 1; grid-row: 1; font-size: var(--fs-md); line-height: 1.35; transition: color .2s var(--ease); }
/* the mark: the page's own arrow while the question is open, the page's own tick once it
   has been answered. Both come out of the sprite at the top of the document. */
.faq-mark { grid-column: 2; grid-row: 1; width: 22px; height: 22px; display: grid; place-items: center; color: var(--gray2); transition: color .2s var(--ease), transform .2s var(--ease); }
.faq-mark svg { grid-area: 1 / 1; width: 18px; height: 18px; fill: currentColor; }
.faq-mark .m-done { display: none; }
.faq-q:hover .faq-mark { color: var(--dg); transform: translateX(3px); }
.faq-q:focus-visible { outline: 2px solid var(--btn); outline-offset: -2px; }

/* answered: the row stays, stops being pressable, and says so three ways at once */
.faq-q[aria-disabled="true"] { cursor: default; }
.faq-q.is-done .faq-text { color: var(--gray2); }
.faq-q.is-done .faq-mark { color: var(--btn); }
.faq-q.is-done .m-ask { display: none; }
.faq-q.is-done .m-done { display: block; }
.faq-q.is-done:hover .faq-mark { color: var(--btn); transform: none; }

/* the proof line, carried over word for word from the rail this list replaced. It is hidden
   until its answer has landed, and it arrives the way the rail's lines did. */
.faq-proof {
  grid-column: 1; grid-row: 2;
  margin-top: 6px;
  font-size: var(--fs-xs); line-height: 1.35; color: var(--gray2);
  opacity: 0; transform: translateY(6px);
  transition: opacity .45s var(--ease), transform .45s var(--ease);
}
.faq-proof[hidden] { display: none; }
.faq-proof.shown { opacity: 1; transform: none; }

/* the thread */
.thread-col { grid-column: 1; grid-row: 2; display: grid; justify-items: center; gap: 14px; }
.thread-col .chat { max-width: none; }
.thread-col .chat-head .avatar { text-transform: lowercase; letter-spacing: .02em; }
.thread-col .chat-body {
  height: 232px;
  min-height: 232px;
  padding: 16px 16px 14px;
  overflow-y: auto;
  overscroll-behavior: contain;
  scroll-behavior: smooth;
}
/* the pane scrolls, so it is a keyboard stop: keep its ring inside the card */
.thread-col .chat-body:focus-visible { outline-offset: -3px; }
/* holds the opening messages at the foot of the pane, collapses once the pane fills */
.flow-pad { flex: 1 1 0; min-height: 0; }
.thread-col .msg { max-width: 78%; }
.chat-cta { align-self: flex-end; margin-top: 4px; }
.chat-cta[hidden] { display: none; }

/* composer, there for looks, it takes no free text */
.composer { display: flex; align-items: center; gap: 10px; padding: 12px 14px; background: var(--white); border-top: 1px solid var(--gray1); }
.composer input {
  flex: 1 1 auto; min-width: 0;
  padding: 11px 16px;
  font-size: var(--fs-xs);
  color: var(--gray2);
  background: var(--white);
  border: 1px solid var(--gray1);
  border-radius: var(--r-full);
}
.composer input:disabled { cursor: not-allowed; }
.composer input::placeholder { color: var(--gray2); opacity: 1; }
.composer .send { width: 38px; height: 38px; flex: none; display: grid; place-items: center; border-radius: 50%; background: var(--btn); color: var(--dg); }
.composer .send svg { width: 17px; height: 17px; }
.thread-col .chat-note { max-width: 100%; padding: 7px 16px; text-align: center; line-height: 1.35; color: var(--gray2); }

/* Two columns from 900 up. The thread spans both rows of the left column, so its own box
   is as tall as the head and the list together and it can hold its place while the page
   scrolls past. It is sticky for exactly as far as the left column is taller than the card,
   which is nothing at all before a question is asked and about 110px once all five have
   been answered and every row has grown its proof line. That is the whole of the travel
   and it is the right amount: on a short laptop screen it keeps the card that is doing the
   answering on screen while the reader works down to the fifth question, and it never
   floats free of the list it belongs to. */
@media (min-width: 900px) {
  .answers {
    grid-template-columns: minmax(0, 42fr) minmax(0, 52fr);
    column-gap: 6%;
    /* the head is sized by its own two lines and the list takes the rest, so the air between
       them is the 36px below and not whatever is left over once the card beside them has
       been measured */
    grid-template-rows: auto 1fr;
    row-gap: 36px;
  }
  .faq-list { grid-row: 2; }
  .thread-col { grid-column: 2; grid-row: 1 / span 2; position: sticky; top: 96px; }
  /* 28px deeper than the pane the centred card carried, which is what puts the foot of
     the card level with the foot of the list beside it before a single question is asked */
  .thread-col .chat-body { height: 260px; min-height: 260px; }
}

/* ---------- 8. the journey (new) ---------- */
.journey { background: var(--journey-bg); }
.journey-list { list-style: none; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; margin-top: var(--gap); }
.journey-step { border-left: 1px solid var(--gray1); padding: 0 24px; }
.journey-step:first-child { border-left: 0; padding-left: 0; }
.journey-step:last-child { padding-right: 0; }
.journey-num { display: block; font-size: var(--fs-micro); color: var(--gray2); font-variant-numeric: tabular-nums; letter-spacing: .05em; margin-bottom: 16px; }
.journey-name { font-size: var(--fs-md); font-weight: 500; color: var(--dg); margin-bottom: 6px; }
.journey-line { font-size: var(--fs-sm); color: var(--gray2); margin: 0; }

/* ---------- 9. the orbit, the twelve channels and the six tools (new) ---------- */
/* Two sections stood here, twelve channels as labelled tiles and the tools as more tiles.
   The eighteen marks arrived, so the two became one object: the Adjoltz bolt on its own at
   the centre, the twelve channels drifting round it on an inner ellipse,
   the six tools on an outer one, the two running opposite ways. The section keeps #channels
   and the outer ring's own list keeps #integrations, so the mega menu link and both footer
   links still land.

   How it moves, which is the whole of it and is all CSS. Each holder rides a motion path.
   offset-path is the ellipse, written once as a path string and used twice, as the d of the
   hairline the ring is drawn with and as the offset-path its holders ride, so the track and
   the travel cannot disagree by a pixel. offset-rotate is 0deg, so a holder never turns to
   face along the path, and offset-anchor is the centre, so it is the middle of the holder
   that sits on the line. One keyframe pair carries offset-distance from 0 to 100 per cent,
   linear and infinite, and the eighteen are spread round their ellipse by a negative
   animation-delay, a twelfth and a sixth of a turn apart. Negative delays mean every holder
   is already mid animation on the first frame; all eighteen started at the same instant and
   share a duration with their ring, so the spacing cannot drift however long the page is
   left open, and no script owns a timeline.

   The tilt. Each ring is rotated -8deg, and every holder inside it, and the centre with
   them, is rotated +8deg. The ellipse leans, the logos stand up.

   Depth. One bolt, one image, drawn twice and cut in two, so the orbit can run through the
   middle of it. The figure paints in four steps, bottom to top: the bolt's tail, clipped to
   everything below the line the orbit crosses it on; the hairline the ring is drawn with;
   the eighteen holders riding on it; and the bolt's shoulder, clipped to everything above
   that same line. The cut is at half the image's height, because the bolt is pinned to the
   ellipse's own centre and the plane of an orbit runs through its centre, and the two clips
   are exact complements, so no row of it is doubled and none is dropped. A holder crossing
   the centre therefore passes over the tail and behind the shoulder, which is what he asked
   for and then asked for again.
   Because the split does the work, no holder carries a depth step of its own and nothing in
   this figure animates a z-index. The inner ring did step its holders once, a z above the
   centre on the near arc and a z below it on the far one, and the two clips have taken that
   job over, at every point of the travel and with no keyframe to keep in step. The step
   could not have parted one holder from another anyway: the closest an inner holder on the
   near arc ever comes to one on the far arc is 106px between centres, at the ends of the
   ellipse where the two arcs meet, and a holder is 95px across, so the pair keeps 11px of air
   and no two of them ever overlap. The outer ellipse passes 25px clear of the bolt at its
   closest, so nothing riding that one reaches the centre at all. The one exception to the
   order above is the holder being read, which is lifted over both halves while its name is
   on screen, and that is written where the name is.

   The two halves have to stay one thing. They are the same file, pinned to the same point by
   the same rule, and the float that lifts them is declared once on the one class they share,
   along with both pauses and the reduced motion stop. Two animations declared apart drift,
   and a drift of a single pixel is the bolt visibly coming apart at the cut.

   What the centre can measure, which is fixed by the two rings and not by taste. The inner
   ellipse comes within 120px of the middle and a holder is 95px across, so the largest centre
   nothing can ever touch is 72.5px of clear radius, about 175px of bolt. The inner ellipse
   cannot be opened out far enough to change that either: 20px is all the air there is between
   the two rings at the top of the figure, so its 120 can only reach about 140 before an inner
   holder and an outer one meet. A 175px bolt is barely half the 325 the centre is actually
   drawn at, which is the opposite of what was asked for, so the crossing stays and the stack
   answers it rather than the size: at any moment one inner holder is passing the bolt, and
   the cut is what decides which side of it that holder is on. The disc had the same crossing,
   23px of it, on the Email holder.

   The sizes, taken up a quarter on 2026-09-17 and set by the room rather than by taste.
   The stage is laid out at 1290 by 690 whatever the window is, centre at 645,345, which is
   the stage's own middle, so the tilt turns the figure about the point the bolt stands on and
   moves neither of them. Inner ellipse 375 by 120, one turn in 34s, clockwise. Outer 588 by
   235, 48s, the other way. Plates 95, ink 50. Every one of those is the figure it replaces
   times 1.25.

   What set 1.25 was the width, and nothing else. The wrap widens to 1340 for this section,
   the same step #platform already takes for the nine, and at 1440 that leaves the container
   1292 with nothing more to be had above it. Leaning by 8 degrees, an ellipse 588 by 235
   measures 583 from the centre to its widest point, and the name pill that hangs under a
   holder reaches about 60 past that holder's centre at the widest of the eighteen names.
   583 and 60 is 643 of the 645 the stage has on each side, so the stage is cut to the figure
   rather than the figure to a round stage: two more pixels of ellipse and a name is clipped.
   The same argument on its side gives the height: 247 from the centre to the top of the
   leaning outer ellipse, 47.5 for the holder riding on it and 35 under the lowest holder for
   its name, so 690 with the centre at 345. The outer ellipse has to be this tall as well as
   this wide: 235 leaves 115px between the two lines over the top of the figure, so 20px of
   air round a 95px holder at the tightest point of the whole figure.

   suite.js measures the container and hands back one number, --ring-s, and the stage is
   scaled by it; nothing else is responsive above 901. Below about 1345px of window the
   container is narrower than 1290 and the figure comes down with it, so the full quarter is
   what the widest desktop gets and the gain tapers from there.

   The marks. Eighteen logos of no common shape: Zoho is a wordmark 2.35 wide for 1 high,
   Telegram and Instagram fill their box square, Google Sheets is a tall document, Salesforce
   a wide cloud. Drawn to one box Zoho would be a hairline and Telegram a slab, so each
   carries --k, a multiplier on --ring-ink, chosen so that the square root of drawn width
   times drawn height is 50px for all eighteen. Two caps sit over that arithmetic: nothing is
   drawn wider than 67px, which is as wide as a mark can be inside a 95px circle and keep its
   air, and nothing taller than 52px, which stops Google Sheets towering over its neighbours.
   SMS, website chat and voice arrived last, as real files rather than logos, and each has its
   own amount of empty canvas around the mark: voice in particular is a thin waveform sitting
   in a mostly transparent square, so its --k has to be larger than a filled icon's just to
   bring the ink up to the same weight. Voice and website chat are near black, which reads
   heavier than a coloured mark of the same size, so both come down a little from where the
   raw arithmetic would put them; SMS is coloured like its neighbours and needed no such step.
   --k is a ratio, so the same eighteen numbers serve the orbit's 50px ink and the tile grid's
   22px. */

/* The ground. The orbit is one cream band between two white ones, so it keeps the full
   section padding on both edges and the change of ground is the only thing parting it from
   the section above it and the message costs below. */
.rings { background: var(--rings-bg); }

/* --- the narrow end: the mark and eighteen tiles, at 900 and below ---
   Written first, so this is what the markup gives on its own: one mark and eighteen tiles,
   every name permanently beside its logo. That is what 900 and every width below it keeps,
   because an orbit shrunk onto a phone is no longer an orbit and a touch screen has no hover
   to raise a name with. The orbit is one media query on top of this, and either way there is
   one list of eighteen in the DOM. */
.rings-fit { --ring-plate: 44px; --ring-ink: 22px; margin-top: var(--gap); }
.ring-track { display: none; }
/* the bolt's second copy, the half that goes in front of the logos, belongs to the orbit and
   nothing else: at 900 and below the centre is a static tile and one whole bolt is all it is */
.ring-bolt-top { display: none; }
.ring-list { list-style: none; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 0; padding: 0; }
/* the twelve and the six are two grids here, parted by the hairline the band itself uses */
.ring.is-out { margin-top: var(--gap); padding-top: var(--gap); border-top: 1px solid var(--gray1); }
/* the mark's tile and a logo's tile are the same object, so they are built once */
.ring-core, .ring-mark {
  display: grid; grid-template-columns: var(--ring-plate) minmax(0, 1fr);
  align-items: center; column-gap: 14px;
  min-height: 68px; padding: 12px 18px;
  border: 1px solid var(--gray1); border-radius: var(--r-md);
}
.ring-core { margin-bottom: var(--gap); }
/* a tile's height is set by the tallest name in its row, and the holder is two levels inside
   the grid item, so the height has to be handed down or a name that wraps leaves a short tile */
.ring-item { display: block; height: 100%; }
.ring-mark { height: 100%; }
/* the plate. A white circle with a hairline and a soft edge, the same object at both ends of
   the page; only its diameter changes. It is twice the ink base in both, so a wide mark keeps
   the same air inside it at either size. */
.ring-plate {
  width: var(--ring-plate); height: var(--ring-plate);
  border-radius: 50%; background: var(--white);
  border: 1px solid var(--gray1); box-shadow: var(--shadow-card);
  display: grid; place-items: center;
}
.ring-logo {
  width: calc(var(--ring-ink) * var(--k, 1));
  height: calc(var(--ring-ink) * var(--k, 1));
  object-fit: contain;
}
/* the centre. The bolt on its own, standing on the band's own ground: no disc, no fill and
   no border behind it. At the narrow end it takes the tile's logo column and stands as tall
   as the plates beside it, which is every pixel that column has to give. */
.ring-disc {
  width: var(--ring-plate); height: var(--ring-plate);
  display: grid; place-items: center;
}
.ring-disc img { width: auto; height: var(--ring-plate); }
.ring-name { font-size: var(--fs-sm); color: var(--dg); line-height: 1.25; }

/* --- the orbit, at 901 and up --- */
@media (min-width: 901px) {
  /* the measured box. --ring-s is written by suite.js and is the only responsive number in
     the whole figure. The height follows it, so the section closes up as the stage shrinks.
     overflow is declared twice on purpose: the stage's own layout box is 1290px wide however
     far it is scaled down, so it has to be cut off or it would widen the page, and clip cuts
     it without making a scroll container that a tab stop could shift. hidden is there for
     anything that has not got clip yet. */
  /* The second section on this page allowed to be wider than the page's own 1200, and it is
     allowed for the reason #platform is: the figure inside it does not fit in 1152 at any
     size worth drawing. .sec-head is capped at 46rem and centred by .center, so the heading
     and its line above the orbit are not touched by this and read exactly as they did; only
     .rings-fit takes the extra width. A max-width cannot exceed the room the section already
     has, so this cannot put the page into horizontal scroll at any width. */
  #channels .wrap { max-width: 1340px; }
  .rings-fit {
    --ring-s: 1;
    --ring-plate: 95px;
    --ring-ink: 50px;
    position: relative;
    height: calc(690px * var(--ring-s));
    overflow: hidden;
    overflow: clip;
  }
  /* Taken out of the flow and centred by hand rather than by an alignment property. Every
     alignment that could centre it, grid, flex or an auto margin, falls back to the start
     edge the moment the subject is wider than the container it sits in, and this subject is
     1290px wide at every width because only its transform shrinks it. So the box is pinned
     at the half way mark and pulled back by half its own width, which centres it whatever it
     measures: the scale then works outward from that same mid line and the figure stays on
     the page's centre line down to the last pixel above 901. */
  .rings-stage {
    position: absolute; left: 50%; top: 0;
    width: 1290px; height: 690px;
    transform: translateX(-50%) scale(var(--ring-s));
    transform-origin: top center;
  }
  /* Each ring is a full stage layer, leaning by the tilt. It takes no pointer of its own, or
     the outer layer would lie over the inner ring and swallow every hover meant for it. */
  .ring {
    position: absolute; inset: 0;
    margin: 0; padding: 0; border: 0;
    transform: rotate(-8deg);
    pointer-events: none;
  }
  .ring.is-in { z-index: 1; }
  /* the seam the two grids carry at the narrow end has to be undone by name here, or it
     turns with the ring as a hairline straight across the figure and the padding under it
     pushes the outer ellipse off the inner one's centre */
  .ring.is-out { z-index: 2; margin: 0; padding: 0; border-top: 0; }
  /* a ring is a stacking context of its own, being turned, so the ring holding the name
     being read has to be lifted as well as the holder inside it, or an inner ring's name can
     be drawn behind an outer ring's holder where the two nearly meet over the top of the
     figure. The keyboard half stands in its own rule rather than sharing a selector list
     with the pointer half, because a browser that does not know :has() throws out the whole
     list it appears in, and losing a hover is a shame where losing a tab stop is a trap. */
  .ring:focus-within { z-index: 4; }
  .ring:has(.ring-mark:hover) { z-index: 4; }

  /* the orbit path itself, the same string the holders ride, drawn as a hairline across the
     bolt's tail and under the holders. It is the line the bolt is cut on: the tail passes
     behind it, the shoulder passes in front of everything. */
  .ring-track { display: block; position: absolute; inset: 0; z-index: 2; color: var(--gray1); }
  .ring-track path { fill: none; stroke: currentColor; stroke-width: 1; }

  /* The bolt is cut in two and the orbit runs through the cut, which is what he asked for and
     asked for again: the tail sits behind the eighteen holders, the shoulder in front of them,
     so a logo crossing the middle passes over the one and under the other. Both halves are the
     same file pinned to the same point by the same rule, so the only thing that tells them
     apart is the clip they carry and the depth they sit at. */
  .ring-core, .ring-bolt-top {
    position: absolute; left: 645px; top: 345px;
    display: block; width: auto; min-height: 0; padding: 0; border: 0; margin: 0;
    transform: translate(-50%, -50%) rotate(8deg);
  }
  .ring-core { z-index: 1; }
  .ring-bolt-top { z-index: 4; }
  /* THE SPLIT POINT, recomputed 2026-09-17 against the enlarged geometry.

     The plane of an orbit runs through its centre, so the line an object is half in front of
     and half behind is the ellipse's CENTRE line, not its lowest point. This was cut at
     87.25% once, taken from where the ellipse's lowest point crosses the bolt. That is the
     bottom of the ring rather than the plane of it, so only the last eighth of the bolt went
     behind and the whole tail stayed in front, slicing into the logos as they came round the
     near side.

     Where it goes now. The rule above pins both halves to left 645, top 345, which is the
     ellipse's own centre, and translates them back by half their own box, so the bolt's
     centre and the ellipse's centre are one point. Both sit inside .ring, and both are turned
     by the same tilt about the same origin, so they stay one point however far the figure
     leans and whatever the stage measures. The bolt is 325 tall, so its top is at
     345 - 162.5 = 182.5, and the cut is
       (ellipse centre y - bolt top y) / bolt height = (345 - 182.5) / 325 = 50.00%.
     It comes out at 50.00% for any stage, any ellipse and any bolt height for as long as that
     one pin holds the two centres together, which is the whole reason the pin is written the
     way it is. Move the bolt off the ellipse centre and this has to be worked out again.

     That is the Saturn split: the near half of the ring passes in front of the bolt's lower
     half, the far half passes behind its upper half. The two insets are exact complements,
     so not one row of pixels is drawn twice or dropped. */
  .bolt-lo { clip-path: inset(50% 0 0 0); }
  .bolt-hi { clip-path: inset(0 0 50% 0); }
  /* The sun the eighteen of them go round. 325 tall, which comes out at 210 wide. It stood
     260 by 168 until the figure was taken up a quarter, and 168 was the diameter the old disc
     carried: the bolt is a narrow diagonal where the disc was a full circle, so it has to
     stand half again as tall to hold the same weight of ink at the middle of the figure.
     The holder is sized by the bolt and not the other way round, so the
     translate that centres each half keeps working off the bolt's own width.
     The float is on the image rather than the holder, which leaves the transform that pins and
     unturns the centre alone. Height and float are written once here, on the one class both
     halves carry, and so are the two pauses and the reduced motion stop further down: give the
     halves an animation each and the two start on different frames and the bolt tears in two. */
  .ring-disc { width: auto; height: auto; }
  .ring-core-name { display: none; }
  .ring .ring-bolt {
    display: block; width: auto; height: 325px;
    animation: bolt-float 9s ease-in-out infinite;
  }

  .ring-list { display: block; margin: 0; padding: 0; }
  /* placed by the path and nothing else: the box is centred on the line at --d per cent
     along it, then turned back upright */
  .ring-item {
    position: absolute; left: 0; top: 0;
    width: var(--ring-plate); height: var(--ring-plate);
    offset-rotate: 0deg;
    offset-anchor: center center;
    offset-distance: calc(var(--d) * 1%);
    transform: rotate(8deg);
    animation: ring-run var(--dur) linear infinite;
    animation-delay: calc(-1 * var(--d) * var(--dur) / 100);
  }
  /* 3 is the middle of the stack: over the bolt's tail at 1 and the track at 2, under the
     bolt's shoulder at 4. These two rules are the ones that decide it, because a plain
     .ring-item z-index loses to them on specificity. */
  .is-in .ring-item {
    --dur: 34s;
    offset-path: path("M1020 345A375 120 0 0 1 270 345A375 120 0 0 1 1020 345");
    z-index: 3;
  }
  .is-out .ring-item {
    --dur: 48s;
    offset-path: path("M1233 345A588 235 0 0 0 57 345A588 235 0 0 0 1233 345");
    z-index: 3;
  }
  .ring-mark {
    position: relative; display: block;
    width: 100%; height: 100%;
    min-height: 0; padding: 0; border: 0; border-radius: 50%;
    pointer-events: auto;
  }
  .ring-plate { width: 100%; height: 100%; }

  /* The name. Held under its own holder and upright with it, up in a tenth of a second and
     gone in under a fifth. It is in the document at every moment for anything reading the
     page aloud, whether it can be seen or not; only its opacity moves. */
  .ring-name {
    position: absolute; top: calc(100% + 9px); left: 50%;
    transform: translate(-50%, -5px);
    padding: 4px 12px; border-radius: var(--r-full);
    background: var(--white); border: 1px solid var(--gray1);
    color: var(--dg); font-size: var(--fs-xs); white-space: nowrap;
    opacity: 0; pointer-events: none;
    transition: opacity .18s var(--ease), transform .18s var(--ease);
  }
  .ring-mark:hover .ring-name,
  .ring-mark:focus .ring-name { opacity: 1; transform: translate(-50%, 0); transition-duration: .1s; }
  /* The holder being read comes out in front of the bolt and its neighbours, or the name it
     is being read by would be drawn behind the bolt it was passing behind. It is the one
     place on this figure where being legible beats being deep, and it only ever holds while
     the orbit is stopped and a name is on screen. */
  .ring-item:hover,
  .ring-item:focus-within { z-index: 9; }
  /* Nothing moves while a name is being read, the orbit and the centre's float alike.
     Split for the same :has() reason as above. */
  .rings-stage:focus-within .ring-item,
  .rings-stage:focus-within .ring-bolt { animation-play-state: paused; }
  .rings-stage:has(.ring-mark:hover) .ring-item,
  .rings-stage:has(.ring-mark:hover) .ring-bolt { animation-play-state: paused; }
}
@keyframes ring-run {
  from { offset-distance: 0%; }
  to { offset-distance: 100%; }
}
/* The centre's float. Six pixels up over four and a half seconds and six back down over the
   next four and a half, eased at both ends so it never arrives or leaves on a hard edge.
   That is under a pixel and a half a second, which is slow enough to read as the thing being
   alive rather than as a movement. Nothing else moves: no turn, no glow and no shadow. */
@keyframes bolt-float {
  from, to { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}
/* No drift at all where drift is not wanted. offset-distance is declared on the holder as
   well as in the keyframes, so with the animation off every holder stands exactly where its
   own --d puts it: the figure is whole, evenly spaced and upright. The depth is not carried
   by any animation, so it is all still here: the tail behind the holders, the track across it
   and the shoulder in front of them all. Every name is one hover or one tab away, as it is
   when the figure turns. */
/* The two animations that ran before the reader arrived.

   The marquee and the orbit are CSS animations with `infinite` on them and no trigger, so both
   were already half a minute in by the time anyone scrolled to them. They are held at their
   first frame until their own section crosses the same line the scroll reveal uses, and
   suite.js releases them by putting .is-live on the section.

   Paused rather than off: .ring-item places every plate with a negative animation-delay from
   its own --d, so paused holds each plate where that delay puts it, spread around the ellipse.
   animation: none would drop all nine to 0 per cent and stack them on one point.

   .js-hold is written by suite.js only when it has an observer to release them with, so with
   no script, or with reduced motion asked for, nothing here matches and both simply run. */
.js-hold .logos:not(.is-live) .marquee-track,
.js-hold #channels:not(.is-live) .ring-item,
.js-hold #channels:not(.is-live) .ring .ring-bolt { animation-play-state: paused; }

/* The hero collage's cards are the third, and the one he was actually looking at. site.css
   gives every card in the active panel `riseUp .72s both, bob 7s infinite`, which starts when
   the stylesheet is parsed. At the top of the page the collage is below the fold, so the cards
   had flown in and settled into their bob before he had scrolled far enough to see them.

   Held at riseUp's first frame, which is opacity 0, 44px low and blurred, so the cards are
   simply not there yet rather than sitting half arrived. Released on a quarter of the stage,
   the same gate collage.js uses to start the reel that fills them, so the cards and their
   content arrive together. The bob inherits the release because both animations are on the
   one declaration. */
.js-hold .world-a .hero .collage:not(.is-live) .collage-panel.active .c { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .ring-item { animation: none; }
  /* and both halves of the centre simply stand there, on the one selector that carries them */
  .ring .ring-bolt { animation: none; }
}

/* ---------- 11. the message costs, proof, highlights and the closing card ----------
   Ported verbatim from version-a.html. All four reuse .costs/.panel/.receipt,
   .proof/.bento/.tile, .more/.more-grid/.more-card and .cta/.card/.form from site.css, and
   the world-a proof/cta tokens in this page's own <head> already match version-a's.
   version-a's own world-a block carries a few rules beyond that token block,
   for this exact ported markup, so they are repeated here rather than in site.css.
   Grounds: the costs panel is a mint gradient that fades to white at its foot, so its
   section stays white and the panel keeps reading as a panel; proof takes --proof-bg, which
   the world block already holds at sage, exactly as version A sets it; the highlights and
   the closing card are both white, and they are the page's one pair of same-ground sections
   with nothing else to part them, so the closing card opens on a hairline. */
.world-a .costs { background: var(--costs-bg); }
.world-a .more { background: var(--more-bg); }
.world-a .cta { background: var(--quote-bg); border-top: 1px solid var(--gray1); }
.world-a .form { border: 1px solid var(--gray1); box-shadow: none; }
.world-a .cta .btn-ghost { color: var(--dg); border-color: var(--gray1); }
.world-a .cta .btn-ghost:hover { background: var(--lg); border-color: var(--dg); }
.world-a .cta .founder blockquote { color: var(--dg); }

/* The languages tile answers back. The eight names were inert spans and are buttons now, so
   site.css's .tile.langs .list span no longer reaches them and the chip is rebuilt here on
   the element that replaced it. It is deliberately the same chip the hero's conversation
   already carries, .lang-chip in site.css, pressed state and all, so the two places on this
   page where a visitor picks a language are recognisably one control. */
.world-a .tile.langs .list button {
  padding: 8px 14px; border-radius: var(--r-full);
  background: var(--tile-chip-bg, var(--white));
  border: 1px solid var(--tile-chip-border, rgba(16, 57, 40, .12));
  font-size: var(--fs-xs); line-height: 1.2; color: var(--dg);
  transition: background-color .2s var(--ease), color .2s var(--ease), border-color .2s var(--ease);
}
.world-a .tile.langs .list button:hover { background: var(--lg); border-color: var(--lg2); }
.world-a .tile.langs .list button[aria-pressed="true"] { background: var(--dg); color: var(--white); border-color: var(--dg); }

/* The exchange the chips swap. Two bubbles and a note, at the tile's smaller sizes, so the
   tile gains three lines and the bento keeps its rows. Every one of the eight is a single
   line at 1440 and at 390 alike, so the panel measures the same in all of them and pressing a
   chip cannot move the tile under it.
   Right to left costs nothing here because nothing is placed by left or right. dir is written
   on the panel, the bubbles are grid items aligned to start and end, and the tail corners are
   the logical ones, so Arabic and Urdu mirror the whole figure on their own. */
.world-a .tile.langs .lang-sample {
  display: grid; gap: 6px; justify-items: start;
  transition: opacity .22s var(--ease);
}
/* No width cap on a bubble. Aligned to start or end in a grid track it is already sized to
   fit and already stops at the panel's own edge, and a cap tight enough to be worth writing
   is a cap that wraps the longest of the eight on a 390 phone and moves the tile under it. */
.world-a .tile.langs .lang-sample p { margin: 0; font-size: var(--fs-xs); line-height: 1.35; }
.world-a .tile.langs .ls-q {
  padding: 7px 12px; border-radius: var(--r-md); border-end-start-radius: 4px;
  background: var(--lg); color: var(--dg);
}
.world-a .tile.langs .ls-a {
  justify-self: end;
  padding: 7px 12px; border-radius: var(--r-md); border-end-end-radius: 4px;
  background: var(--dg); color: var(--white);
}
.world-a .tile.langs .ls-note { font-size: var(--fs-micro); color: var(--tile-muted, var(--gray2)); }
/* The swap. suite.js drops the panel to nothing with no transition at all, writes the new
   exchange into it and takes the class off again, and the rule above carries it back up. One
   fade, one way, no timer to fall out of step with. */
.world-a .tile.langs .lang-sample.is-swap { opacity: 0; transition: none; }

/* ---------- 12. footer (new) ----------
   Dark, on var(--dg), independent of site.css's .footer component so the
   world-a override that keeps that component light on version-a never touches it. */
.suite-footer { background: var(--dg); color: var(--w80); padding-block: 64px 32px; }
.suite-footer a { color: var(--white); }
.suite-footer-cols { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 32px; }
.suite-footer-col h4 { font-size: var(--fs-micro); font-weight: 400; text-transform: uppercase; letter-spacing: .06em; color: var(--w60); margin-bottom: 16px; }
.suite-footer-col ul { list-style: none; display: grid; gap: 10px; }
.suite-footer-col li { font-size: var(--fs-sm); line-height: 1.3; overflow-wrap: break-word; }
.suite-footer-bottom { margin-top: 48px; padding-top: 22px; border-top: 1px solid var(--w20); display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; font-size: var(--fs-xs); color: var(--w60); }
.suite-footer-bottom a { color: inherit; }

/* ---------- breakpoints ---------- */
@media (max-width: 1040px) {
  /* no mega menu below here, the header falls back to the mobile menu */
  .world-a .suite-nav .has-mega { display: none; }
  .world-a .suite-nav .mega { display: none; }
  .world-a .agents .feat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 900px) {
  .journey-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px 24px; }
  .journey-step { border-left: 0; border-top: 1px solid var(--gray1); padding: 20px 0 0; }
  .journey-step:nth-child(-n+2) { border-top: 0; padding-top: 0; }
  .suite-footer-cols { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
  .world-a .fact-row { grid-template-columns: 1fr; row-gap: 24px; }
  .world-a .fact { padding: 20px 0 0; border-left: 0; border-top: 1px solid var(--gray1); }
  .world-a .fact:first-child { border-top: 0; padding-top: 0; }
}
@media (max-width: 560px) {
  .world-a .agents .feat-grid { grid-template-columns: minmax(0, 1fr); }
  .ring-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* two tiles across a 390 phone leave the name about 64px and Messenger wants 82, so the
     plate and the gutters give some of it back. One number moves the plate, the ink inside
     it and the column that holds them. */
  .rings-fit { --ring-plate: 34px; --ring-ink: 17px; }
  .ring-core, .ring-mark { column-gap: 10px; padding: 12px 14px; }
  .journey-list { grid-template-columns: 1fr; }
  .journey-step { border-top: 0; padding-top: 0; }
  .journey-step:not(:first-child) { border-top: 1px solid var(--gray1); padding-top: 20px; }
  .suite-footer-cols { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
  .thread-col .msg { max-width: 86%; }
  .composer input { font-size: var(--fs-micro); }
}
/* ---------- 13. the responsive and motion pass ----------
   Every rule below fixes a fault measured at one of 1440, 1180, 1100, 1040, 960, 900, 760
   or 390. Nothing here restyles a section that was already right at its width. */

/* the header is sticky at every width, so a section an anchor points at holds its own room
   under it. Without this the top of the target section, and at the narrow widths its
   heading with it, lands behind the bar. The orbit's outer list is named here beside them,
   because #integrations sits on that list rather than on a section of its own. */
main section[id], .ring-list[id] { scroll-margin-top: 88px; }

/* Single words left on a line of their own: the service list and the journey steps at 1440,
   the trust row and the section intros and the client quote lower down. Each of these runs
   at its column's width rather than at a cap of its own, so a max-width in ch would have to
   be tuned per string. Pretty only moves the last line, so at every width where the line
   already ended on two words or more nothing here changes at all. */
.world-a .orb-line, .journey-line, .world-a .fact-line,
.world-a .sec-head p, .world-a .tile.quote blockquote { text-wrap: pretty; }

/* The hero collage's mint panel bleeds 45px past the stage on each side, so the panel is
   1210px wide before site.css scales it. site.css steps that scale down at 1180 and again
   at 1020; in the band between those two steps the panel measures 1041px against a window
   only 1030px wide, and the page gains a horizontal scrollbar. Taking the 1020 step early
   is the whole fix. The band is bounded at the bottom so the 900 and 760 steps below it,
   which come earlier in source order than this file, still win at their own widths, and it
   reaches 20px past where the panel starts fitting again so a wider scrollbar than this
   machine's cannot walk the fault back in. */
@media (max-width: 1080px) and (min-width: 1021px) {
  .collage-wrap { --s: .74; }
}
/* The same fault, one step up the ladder. Unscaled the panel is 1210px wide and it is
   centred on the content box, which is the window less the scrollbar, so it needs a window
   of 1220 before it fits. site.css holds the scale at 1 all the way down to 1181 and only
   steps to .86 at 1180, which leaves 1181 to 1219 measuring 1210 against 1171 to 1209 of
   room: 15px of horizontal scroll at 1190, 10 at 1200, 5 at 1210. Taking the .86 step early
   closes it, and .86 puts the panel at 1041, inside the 1171 the narrowest width in the band
   has. The band is bounded at both ends: 1181 hands back to site.css's own 1180 step below
   it, and 1219 is the last width that needs anything, so 1220 and every width above it,
   1440 included, keeps the collage at full size. */
@media (max-width: 1219px) and (min-width: 1181px) {
  .collage-wrap { --s: .86; }
}

/* The closing card runs two columns down to 900, and in the band just above that its lead
   is a few pixels too long for the column and drops its last word on to a line of its own. */
@media (max-width: 1000px) and (min-width: 901px) {
  .world-a .cta .lead { max-width: 25ch; }
}

/* Below 900 both of these closing lines lose the cap that was holding their shape and
   strand their last word. site.css sets .costs .big to max-width none at this width, which
   is what lets it happen, so the cap is restored here for this page only. */
@media (max-width: 900px) {
  .world-a .agents-close { max-width: 44ch; margin-inline: auto; }
  /* the cap cures the break down to about 480, below which the column is only sixteen
     characters wide and no cap can put two words on the last line. Balancing the lines is
     the only cure left that is not a hard break. */
  #costs .big { max-width: 18ch; text-wrap: balance; }
  /* three facts across at this width leave the last one dropping a single long word on to
     a line of its own, which pretty cannot pull up. Balancing the three lines does. */
  .world-a .fact-line { text-wrap: balance; }
}

/* touch sizes, measured on the 390 pass and applied from the tablet width down */
@media (max-width: 760px) {
  .world-a .suite-nav .logo { padding-block: 2px; }
  .world-a .suite-nav .btn-sm { padding-block: 14px; }
  .topline .wrap a { min-height: 44px; }
  /* The slider this used to raise to 44px no longer draws: under .js-dial it is a clipped
     1px box and the knob is what a thumb lands on. The 120px dial clears the target on its
     own. The typed field is the one that still needed the floor restated. */
  #costs .dial-num { min-height: 44px; }
  .suite-footer-col ul { gap: 2px; }
  .suite-footer-col li a { display: block; padding-block: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  /* the sticky header's rule and shadow arrive on scroll, so they arrive at once */
  .world-a .suite-nav { transition: none; }
  .world-a .suite-nav .nav-trigger .chev { transition: none; }
  .world-a .suite-nav .mega,
  .world-a .suite-nav .has-mega.is-open .mega { transition: none; }
  /* the proof line is simply there under its question, with no rise and no fade */
  .faq-proof { opacity: 1; transform: none; transition: none; }
  .faq-text, .faq-mark { transition: none; }
  .faq-q:hover .faq-mark { transform: none; }
  .thread-col .chat-body { scroll-behavior: auto; }
}

/* ---------- 14. the middle of the page gets something to look at ----------
   Four sections between the trust row and the cream band were type on a coloured ground and
   nothing else. Nothing new is drawn here. The demonstrations are the components site.css
   already carries, with their own class names and their own nesting, and the photographs are
   the stock this project already shot. What this section does is take those cards off the
   collage's camera rig, hold them still, and recolour the ones that now sit on dark green. */

/* --- 14a. the four agents, each with a demonstration under its text ---
   The card grows a third row. The copy row takes the slack, so the demonstrations line up
   along the bottom of the four cards whatever length the paragraph above them runs to, and
   the min-height puts all four dividing rules at one height as well. */
.world-a .agents .feat { grid-template-rows: auto 1fr auto; align-content: stretch; gap: 14px; padding: 22px; }
.world-a .agents .agent-demo {
  display: grid; align-content: end; gap: 8px;
  min-height: 144px; padding-top: 14px;
  border-top: 1px solid var(--feat-card-border);
}
/* Four demonstrations are 400px of new section, so the block's own vertical rhythm is taken
   in a notch to hold it near the 900px it was asked to stay under. Nothing else on the page
   moves: these two rules name the agents block and nothing else. */
.world-a .agents .sec-head { margin-bottom: clamp(28px, 3vw, 40px); }
.world-a .agents-close { margin-top: clamp(28px, 3vw, 36px); }

/* Every component below was built for a white or a cream ground, so each of its surfaces,
   rules and type colours is reassigned from the dark card's own tokens. The customer's
   message takes a white wash of the ground, the brand's reply keeps the mint of a sent
   WhatsApp bubble with the dark green type it carries everywhere else on this site, and the
   quiet line is the card's --feat-card-muted rather than the light world's --gray2, which is
   the one colour that fails on this ground. */
.world-a .agents .agent-demo .chat {
  max-width: none; background: none; border: 0; border-radius: 0; box-shadow: none;
  color: var(--feat-card-fg); display: grid; gap: 10px;
}
.world-a .agents .agent-demo .chat-head {
  padding: 0 0 10px; gap: 8px; background: none; color: var(--feat-card-fg);
  border-bottom: 1px solid var(--feat-card-border);
}
.world-a .agents .agent-demo .chat-head .avatar { width: 24px; height: 24px; font-size: 10px; }
.world-a .agents .agent-demo .chat-head b { font-size: var(--fs-micro); }
.world-a .agents .agent-demo .chat-head .status { font-size: 10px; }
.world-a .agents .agent-demo .chat-head .status::before { width: 6px; height: 6px; }
.world-a .agents .agent-demo .chat-body { padding: 0; gap: 6px; min-height: 0; background-image: none; }
.world-a .agents .agent-demo .msg {
  max-width: 94%; padding: 7px 10px; border-radius: 12px;
  font-size: var(--fs-micro); line-height: 1.4; box-shadow: none;
}
.world-a .agents .agent-demo .msg.in { background: var(--w15); color: var(--feat-card-fg); border-top-left-radius: 4px; }
.world-a .agents .agent-demo .msg.out { background: var(--lg); color: var(--dg); border-top-right-radius: 4px; }
/* the co-pilot's suggestion is not a sent message, so it does not wear a sent bubble's
   colour: an outline, the ground showing through, and no tick anywhere near it */
.world-a .agents .agent-demo .msg.draft { background: none; color: var(--feat-card-fg); border: 1px dashed var(--w40); }
.world-a .agents .agent-demo .chat-note {
  background: none; border-color: var(--feat-card-border); color: var(--feat-card-muted);
  font-size: 10px; padding: 3px 10px;
}

.world-a .agents .agent-demo .bcast { max-width: none; gap: 0; }
.world-a .agents .agent-demo .bcast-card {
  background: var(--w15); border-color: var(--feat-card-border); border-radius: var(--r-md);
  box-shadow: none; color: var(--feat-card-fg);
}
.world-a .agents .agent-demo .bcast-to {
  padding: 9px 12px; font-size: 10px; color: var(--feat-card-muted);
  border-bottom-color: var(--feat-card-border);
}
.world-a .agents .agent-demo .bcast-to .tg { background: var(--lg); color: var(--dg); font-size: 10px; padding: 3px 8px; }
.world-a .agents .agent-demo .bcast-text { padding: 10px 12px 12px; font-size: var(--fs-micro); line-height: 1.4; }

/* the analyst's sample. The label is the smallest type in the block by design: it has to be
   read before the figures are, and it is the only thing on this page that licenses them. */
.world-a .agents .agent-demo .demo-label { font-size: var(--fs-micro); color: var(--feat-card-muted); }
.world-a .agents .agent-demo .report { max-width: none; }
.world-a .agents .agent-demo .report table { margin-top: 0; font-size: var(--fs-micro); }
.world-a .agents .agent-demo .report td { padding: 7px 0; border-top-color: var(--feat-card-border); color: var(--feat-card-muted); }
.world-a .agents .agent-demo .report tr:first-child td { border-top: 0; padding-top: 0; }
.world-a .agents .agent-demo .report td:last-child { color: var(--feat-card-fg); }

/* --- 14b. the journey, each step opens with the moment it describes ---
   All four columns have to hold the same inner width, or four boxes of one aspect ratio end
   at four different heights and the numbers under them stop lining up. So the list is pulled
   out by the wrap's own padding and every step is padded instead: the outer two boxes still
   sit flush with the wrap, and all four measure the same. */
.journey-list { margin-inline: calc(var(--pad) * -1); }
.journey-step:first-child { padding-left: var(--pad); }
.journey-step:last-child { padding-right: var(--pad); }
/* width and height are on every img as well, so the box is reserved twice over and a photo
   can neither stretch nor reflow the column while it loads */
.journey-shot {
  aspect-ratio: 4 / 3; margin-bottom: 18px;
  border-radius: var(--r-md); overflow: hidden; background: var(--white);
}
.journey-shot img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* step three is a confirmation rather than a scene, so its box holds a receipt. It fills the
   same ratio as the three photographs, so the row still reads as one line of four. */
.journey-shot .receipt {
  height: 100%; border-radius: inherit; box-shadow: none;
  display: grid; grid-template-rows: auto repeat(3, 1fr);
}
.journey-shot .receipt-head { flex-direction: column; align-items: flex-start; gap: 2px; padding: 12px 16px; }
.journey-shot .receipt-row { padding: 8px 16px; font-size: var(--fs-xs); align-items: center; }

/* --- 14c. the nine services orbit the one screen ---
   Version A's inbox, markup unchanged, with the nine arranged around it as labels and a
   hairline arrow leaving each one for the screen. Written up from the narrow end: what the
   markup gives on its own is a stack, a figure above a list, and that is what every width
   below 1100 keeps. The orbit is one media query on top of that, and the arrows only exist
   inside it. Nothing here is a colour: the border and the stroke are both --gray1, the two
   type sizes and the radius are tokens, and the ground is the section's own white. */
.world-a .desk-stage { max-width: 880px; margin-inline: auto; }
/* 240px of contact column leaves the avatar, a 130px preview and a tag to share 208px, and
   the two longest tags lose their last letters to the column rule. Forty more pixels is the
   whole fix, and it stays inside this page: site.css is not touched and version A keeps the
   column width it was built with. */
.world-a .desk-stage .inbox { min-height: 380px; grid-template-columns: 280px 1fr; }
.world-a .desk-stage .inbox-thread .msg { max-width: 420px; }
.world-a .desk-stage .mock-caption { margin-top: 12px; }

/* the stack: the screen, then the nine under it. The figure no longer carries the gap, the
   container does, so the orbit can close it to nothing without fighting a margin. */
.world-a .orbit { display: grid; row-gap: clamp(34px, 4vw, 50px); }
.world-a .orbit-list { list-style: none; display: grid; gap: 12px; }
/* A bubble, not a cell in a table. Two columns: the mark, then the name over the line, and
   the mark spans both rows exactly as the mega menu's .ico does, which is also where its
   markup comes from. What makes it a bubble rather than a box is the order of the three
   values below: the radius goes one step up the ladder to --r-lg, --shadow-float lifts it
   off the white ground, and only once the shadow is carrying the shape does the border
   drop from --gray1 to --lg, a hairline that closes the edge without drawing a cell around
   it. No colour here is new: --lg and --lg2 are the page's two mints and --shadow-float is
   the same lift the hero's own cards sit on. */
.world-a .orb-item {
  position: relative; list-style: none;
  display: grid; grid-template-columns: 28px minmax(0, 1fr);
  align-content: start; column-gap: 10px; row-gap: 2px;
  background: var(--white); border: 1px solid var(--lg); border-radius: var(--r-lg);
  padding: 13px 16px; box-shadow: var(--shadow-float);
}
/* the plate reads as the name's mark, so it sits at the top of the text block rather than
   drifting to the middle of it on the bubbles whose line runs to three */
.world-a .orb-mark {
  grid-column: 1; grid-row: 1 / span 2; align-self: start;
  width: 28px; height: 28px; border-radius: var(--r-sm);
  background: var(--lg); color: var(--dg);
  display: grid; place-items: center;
}
.world-a .orb-mark svg { width: 16px; height: 16px; fill: currentColor; }
.world-a .orb-name { grid-column: 2; grid-row: 1; font-size: var(--fs-sm); font-weight: 500; color: var(--dg); line-height: 1.3; }
.world-a .orb-line { grid-column: 2; grid-row: 2; font-size: var(--fs-xs); color: var(--gray2); line-height: 1.35; }
/* Nothing in this section is clickable, so the hover has to stay under the line where a
   pointer would start expecting a link: three pixels of lift, the shadow opening out the
   way a shadow does when an object rises, and the hairline and the plate each going one
   mint deeper. No cursor change, no colour change to the type, no scale. It is behind
   (hover: hover) so a touch screen cannot stick in it, and the lift is taken off again in
   the reduced motion block at the end of this part. */
@media (hover: hover) {
  .world-a .orb-item {
    transition: transform .3s var(--ease), box-shadow .3s var(--ease), border-color .3s var(--ease);
  }
  .world-a .orb-item:hover {
    transform: translateY(-3px);
    border-color: var(--lg2); box-shadow: var(--shadow-panel);
  }
  .world-a .orb-item:hover .orb-mark { background: var(--lg2); }
}
/* The arrows are decoration and they are aria-hidden in the markup at every width, but an
   arrow pointing at nothing is worse than no arrow, so below the orbit they leave the
   layout outright rather than being drawn somewhere harmless. */
.world-a .orb-arrow { display: none; }

/* 760 up to the orbit: three across, three down, the screen still above them */
@media (min-width: 760px) {
  .world-a .orbit-list { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
}

/* 1100 and up: the orbit itself.
   Five tracks in effect: a bubble column, a gutter the arrow exactly fills, the screen, the
   second gutter, the second bubble column. The gutters are the grid's own column-gap, so
   there is one number to change and the arrow follows it: the svg is the width of the gutter
   and scales inside its viewBox, with a non-scaling stroke so the hairline stays a hairline.
   The four rows are equal fractions of the screen's own height, which is what spreads four
   labels evenly down a column without a single hand placed offset.
   The wrap widens here, and only here. Nine labels plus two gutters plus a screen wide
   enough to still read as a screen does not fit inside 1200, and the alternative was to
   shrink the inbox until it was an icon of itself. A max-width cannot exceed the room the
   section already has, so this cannot put the page into horizontal scroll at any width. */
@media (min-width: 1100px) {
  #platform .wrap { max-width: 1340px; }
  .world-a .orbit {
    --orb-side: clamp(158px, 14vw, 206px);
    --orb-gap: clamp(58px, 5.2vw, 78px);
    grid-template-columns: var(--orb-side) minmax(0, 1fr) var(--orb-side);
    grid-template-rows: repeat(4, 1fr) auto;
    column-gap: var(--orb-gap);
    row-gap: 0;
    align-items: center;
  }
  /* the auto margins that centre the screen in the stack would, on a stretched grid item,
     shrink it to the caption's max-content and leave every arrow stopping short of it. In
     the orbit the column is the centring, so the figure simply fills it. */
  .world-a .orbit .desk-stage { grid-column: 2; grid-row: 1 / 5; max-width: none; margin-inline: 0; }
  /* the list box steps out of the layout so its nine items are the orbit's own grid items */
  .world-a .orbit-list { display: contents; }
  /* The bubble column is 158px at the narrow end of the orbit and the mark and its gap take
     37 of them. Four pixels off each side and one off the gap give the name 99px, and the
     longest of the nine, Website chat, measures 96.5 at --fs-sm 500: every name stays on one
     line at every width the orbit runs at, which is what keeps a bubble inside its row.
     Padding is not what the arrow is measured from, the border is, so none of this moves an
     arrow by a pixel. */
  .world-a .orb-item { padding: 11px 10px; column-gap: 9px; }
  /* the screen is narrower here than the 880 it gets in the stack, so the contact column and
     the four figures beside it give back what they can spare rather than clipping */
  .world-a .orbit .inbox { grid-template-columns: minmax(0, clamp(232px, 36%, 280px)) minmax(0, 1fr); }
  /* the preview line is nowrap with an ellipsis and a fixed cap, so in a contact column this
     narrow its cap becomes the row's minimum and pushes the tag out through the column rule.
     A track that may shrink past its content, and a cap that is whatever room is left, gives
     the ellipsis the job it was there to do and keeps every tag whole. */
  .world-a .orbit .inbox-row { grid-template-columns: 34px minmax(0, 1fr) auto; }
  .world-a .orbit .inbox-row small { max-width: 100%; }
  .world-a .orbit .inbox-stats div { padding: 14px 12px; }

  /* four down the left, four down the right, the ninth under the screen */
  .world-a .orb-item:nth-child(-n+4) { grid-column: 1; }
  .world-a .orb-item:nth-child(n+5):nth-child(-n+8) { grid-column: 3; }
  .world-a .orb-item:nth-child(1), .world-a .orb-item:nth-child(5) { grid-row: 1; }
  .world-a .orb-item:nth-child(2), .world-a .orb-item:nth-child(6) { grid-row: 2; }
  .world-a .orb-item:nth-child(3), .world-a .orb-item:nth-child(7) { grid-row: 3; }
  .world-a .orb-item:nth-child(4), .world-a .orb-item:nth-child(8) { grid-row: 4; }
  .world-a .orb-base { grid-column: 2; grid-row: 5; justify-self: center; margin-top: 68px; }

  /* the arrow belongs to the bubble: it starts at that bubble's inner edge, crosses the
     gutter and stops short of the screen, and it is positioned against the bubble so it can
     never drift from the row it names. */
  .world-a .orb-arrow {
    display: block; position: absolute;
    width: var(--orb-gap); height: auto;
    top: 50%; left: 100%; transform: translateY(-50%);
  }
  /* the right column is the same drawing, mirrored, so both columns lean inward */
  .world-a .orb-item:nth-child(n+5):nth-child(-n+8) .orb-arrow {
    left: auto; right: 100%; transform: translateY(-50%) scaleX(-1);
  }
  /* Rows three and four sit below the screen's middle, so their arrows bend up into it.
     Both of these are written as a pair of nth-child so they weigh exactly what the mirror
     rule above weighs and win it on order rather than on an accident of specificity. */
  .world-a .orb-item:nth-child(n+3):nth-child(-n+4) .orb-arrow { transform: translateY(-50%) scaleY(-1); }
  .world-a .orb-item:nth-child(n+7):nth-child(-n+8) .orb-arrow { transform: translateY(-50%) scaleX(-1) scaleY(-1); }
  /* the ninth arrow is the only vertical one: it leaves the top of the bubble and points up */
  .world-a .orb-base .orb-arrow {
    top: auto; left: 50%; right: auto; bottom: 100%;
    width: 40px; margin-bottom: 6px; transform: translateX(-50%);
  }
  .world-a .orb-arrow path {
    fill: none; stroke: var(--gray1); stroke-width: 1;
    stroke-linecap: round; stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
  }
}

/* --- 14d. one reveal, and only ever the one ---
   This stylesheet hides nothing on its own. [data-reveal] is written by suite.js, and
   suite.js writes it only once it holds an IntersectionObserver and has checked that motion
   is wanted. Script off, script broken, script blocked, or reduced motion: no element on
   this page is ever transparent. suite.js also strips both the attribute and the class once
   an element has landed, so nothing is left carrying a transition it has no further use for,
   and no hover already on the page has its timing changed by this. */
.js-reveal [data-reveal] { opacity: 0; transform: translateY(12px); }
.js-reveal [data-reveal].is-in {
  opacity: 1; transform: none;
  transition: opacity .5s var(--ease), transform .5s var(--ease);
}
/* The orbit's arrows draw in behind the bubble they leave, on the same reveal and with no
   second system to run them: the bubble is the element suite.js watches, the arrow is inside
   it, and pathLength="100" makes one dash length fit both the long horizontal arrow and the
   short vertical one. transition-delay: inherit is the whole trick. suite.js writes the
   stagger as an inline transition-delay on the bubble, so the stroke reads its own delay
   straight off its ancestor and cannot fall out of step with the card it belongs to. The
   .45s finishes inside the 620ms after which suite.js strips the attribute and both of these
   rules stop matching, so the stroke is a plain solid hairline by the time anything changes.
   No [data-reveal], no .js-reveal, and there is no dash here at all. */
.js-reveal .orb-item .orb-arrow { transition-delay: inherit; }
.js-reveal .orb-item[data-reveal] .orb-arrow path { stroke-dasharray: 100; stroke-dashoffset: 100; }
.js-reveal .orb-item[data-reveal].is-in .orb-arrow path {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset .45s var(--ease);
  transition-delay: inherit;
}

/* --- 14e. the widths ---
   Two columns of steps at 900 and one at 560 give each box far more width than the four
   across version ever had, and a 4:3 box that wide is a poster. Capping the height crops it
   instead, and since every column still measures the same the row stays level. */
@media (max-width: 900px) {
  .journey-list { margin-inline: 0; }
  .journey-step, .journey-step:first-child, .journey-step:last-child { padding: 20px 0 0; }
  .journey-step:nth-child(-n+2) { padding-top: 0; }
  .journey-shot { max-height: 240px; }
}
@media (max-width: 560px) {
  .journey-step, .journey-step:first-child, .journey-step:last-child { padding: 0; }
  .journey-step:not(:first-child) { padding-top: 20px; }
  .journey-shot { max-height: 220px; }
}
/* below the two column break a demonstration no longer shares a row with three others, so
   the rule it hangs from can sit wherever the copy above it ends */
@media (max-width: 1040px) {
  .world-a .agents .agent-demo { min-height: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .js-reveal [data-reveal],
  .js-reveal [data-reveal].is-in { opacity: 1; transform: none; transition: none; }
  /* suite.js never writes [data-reveal] under reduced motion, so neither of the two rules
     above nor the dash pair ever matches. Restated anyway, because a half drawn arrow is the
     one failure this stylesheet would not be able to explain. */
  .js-reveal .orb-item[data-reveal] .orb-arrow path,
  .js-reveal .orb-item[data-reveal].is-in .orb-arrow path {
    stroke-dasharray: none; stroke-dashoffset: 0; transition: none;
  }
  /* the bubbles keep the warmer hairline and the warmer plate, which are not motion, and
     lose the lift and the timing that carried it. This block is the last word on .orb-item
     in this file, so it wins the hover rule on order at equal weight. */
  .world-a .orb-item { transition: none; }
  .world-a .orb-item:hover { transform: none; }
  /* the languages tile swaps its exchange on the instant instead of fading it. suite.js
     still writes and clears .is-swap, and with no transition on either state the panel is
     simply holding the new language by the time the class comes off. */
  .world-a .tile.langs .lang-sample { transition: none; }
}

/* ---------- 15. the hero's three offers ----------
   The hero used to hold three views of one product. It now holds the three things the
   company sells, so tab one keeps the broadcast card and its reel exactly as they were and
   tabs two and three are built here out of the components site.css already carries:
   .c-phone and its bubble, .stat-strip, .bcast-text and its caret, .staged and its [data-t]
   reveal, .tickmark, the stamp keyframe. Nothing below invents a colour. Every value is a
   token from site.css or the rgba(16, 57, 40, x) this project already borders and rules its
   cards with, and the one state that had no token, a refusal at the door, is drawn in the
   greys the page already uses for secondary type rather than in a colour bought for it.

   How the two reels are driven is documented at the head of their block in suite.js. What
   matters here is the contract this stylesheet keeps with it: the only things hidden below
   are [data-t] parts inside a .staged card, the mechanism site.css already owns and already
   forces visible under reduced motion. Script off, script broken, or motion reduced, and
   both panels are complete and readable. */

/* --- the four photographs ---
   Each slot holds the real photograph at a fixed aspect ratio, set here rather than left to
   the photograph, so the box and the photograph are always the same height at the same width. */
.collage .shot {
  aspect-ratio: 16 / 9;
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-float);
  overflow: hidden;
}
/* the two close ups are 4:3, so a scene and a detail read as two different kinds of frame */
.collage .shot.shot-4x3 { aspect-ratio: 4 / 3; }
.collage .shot img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* --- Expo Desk, the lead card ---
   What the badge gave us, in the order the person at the desk hears it. */
.collage .lead-card {
  background: var(--white); border: 1px solid rgba(16, 57, 40, .12);
  border-radius: var(--r-lg); box-shadow: var(--shadow-float);
  overflow: hidden; color: var(--black1);
}
.collage .lead-head { display: flex; align-items: center; gap: 10px; padding: 13px 18px; background: var(--lg); }
.collage .lead-mark { width: 26px; height: 26px; flex: none; border-radius: 8px; background: var(--dg); color: var(--white); display: grid; place-items: center; }
.collage .lead-mark svg { width: 14px; height: 14px; fill: currentColor; }
.collage .lead-head b { font-size: 15px; font-weight: 500; color: var(--dg); }
.collage .lead-row { padding: 12px 18px; font-size: var(--fs-xs); color: var(--gray2); border-top: 1px solid rgba(16, 57, 40, .08); }
.collage .lead-row.is-name { font-size: var(--fs-sm); font-weight: 500; color: var(--dg); }

/* --- Expo Desk, the clock ---
   A .stat-strip of one, because .stat-strip b is what collage.js's climbStats reaches for and
   keeping it is what makes this figure climb on collage.js's own countUp rather than on a
   second copy of that code living in this project. The 2ch floor holds the slot two digits
   wide from the first frame, so a number counting up from nothing never shifts the card. */
.collage .timer { grid-template-columns: 1fr; gap: 6px; }
.collage .timer > div { padding: 14px 12px 12px; }
.collage .timer b {
  display: inline-block; min-width: 2ch;
  font-size: 52px; line-height: 1; color: var(--dg);
  font-variant-numeric: tabular-nums;
}
.collage .timer span { display: block; margin-top: 8px; font-size: var(--fs-xs); color: var(--gray2); }
.collage .timer p {
  max-width: none; text-align: center;
  font-size: var(--fs-micro); color: var(--dg);
  background: var(--lg); border-radius: 12px; padding: 8px 10px;
}

/* --- Expo Desk, the fortnight ---
   Two sent and two still to go, so the schedule reads as something already running rather
   than a plan drawn up for the mockup. */
.collage .plan {
  background: var(--white); border: 1px solid rgba(16, 57, 40, .12);
  border-radius: var(--r-lg); box-shadow: var(--shadow-float);
  overflow: hidden; color: var(--black1);
}
.collage .plan-head { padding: 13px 18px; border-bottom: 1px solid rgba(16, 57, 40, .10); }
.collage .plan-head b { font-size: 15px; font-weight: 500; color: var(--dg); }
.collage .plan-rows { list-style: none; }
.collage .plan-rows li {
  display: flex; align-items: center; gap: 11px;
  padding: 11px 18px; font-size: var(--fs-xs); color: var(--gray2);
  border-top: 1px solid rgba(16, 57, 40, .06);
}
.collage .plan-rows li:first-child { border-top: 0; }
.collage .plan-mark {
  width: 18px; height: 18px; flex: none; border-radius: 50%;
  border: 1px solid var(--gray1); background: var(--white);
  display: grid; place-items: center; color: var(--dg);
}
.collage .plan-mark svg { fill: currentColor; }
.collage .plan-rows li.is-sent { color: var(--dg); }
.collage .plan-rows li.is-sent .plan-mark { background: var(--lg); border-color: transparent; }
/* the mark lands on the row that has just lit, the same stamp site.css gives Meta's approval
   on the broadcast card, so four rows arriving one at a time read as a schedule filling in */
.collage .staged .plan-rows li[data-t].shown .plan-mark { animation: stamp .4s cubic-bezier(.2, 1.5, .4, 1) both; }

/* --- Guest Desk, the invitation ---
   The three lines carry site.css's own .bcast-text, so the caret, its blink, its removal
   under reduced motion and the .typed slot are the broadcast card's and not a second set. */
.collage .invite-card {
  position: relative; text-align: center; color: var(--dg);
  background: var(--cream); border: 1px solid rgba(16, 57, 40, .12);
  border-radius: var(--r-lg); box-shadow: var(--shadow-float);
  padding: 30px 26px 28px;
}
.collage .invite-rule { position: absolute; inset: 8px; border: 1px solid rgba(16, 57, 40, .16); border-radius: 18px; pointer-events: none; }
.collage .invite-line { position: relative; padding: 0; font-size: var(--fs-sm); line-height: 1.5; color: var(--gray2); }
.collage .invite-line + .invite-line { margin-top: 6px; }
.collage .invite-line.is-big { font-size: 26px; line-height: 1.25; color: var(--dg); margin-bottom: 14px; }

/* --- Guest Desk, the pass ---
   The barcode is drawn in the markup as a run of bars at three widths. It encodes nothing and
   scans as nothing, which is the whole point of drawing it rather than generating one. */
.collage .pass {
  background: var(--white); border: 1px solid rgba(16, 57, 40, .12);
  border-radius: var(--r-lg); box-shadow: var(--shadow-float);
  overflow: hidden; color: var(--black1);
}
.collage .pass-code { padding: 20px 18px 16px; color: var(--dg); }
.collage .pass-code svg { display: block; width: 100%; height: auto; }
/* the perforation a real ticket tears along, and the only dashed rule on the card */
.collage .pass-foot {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 13px 18px 15px; border-top: 1px dashed rgba(16, 57, 40, .24);
}
.collage .pass-foot b { font-size: var(--fs-md); font-weight: 500; color: var(--dg); }
.collage .pass-foot span { font-size: var(--fs-micro); color: var(--gray2); }

/* --- Guest Desk, the door ---
   Two reads off one scanner. The first is admitted and the second is not, and the second is
   the thing being sold, so it is the loudest beat in the reel. */
.collage .gate {
  background: var(--white); border: 1px solid rgba(16, 57, 40, .12);
  border-radius: var(--r-lg); box-shadow: var(--shadow-float);
  overflow: hidden; color: var(--black1);
}
.collage .gate-head { display: flex; align-items: center; gap: 10px; padding: 13px 18px; border-bottom: 1px solid rgba(16, 57, 40, .10); }
.collage .gate-mark { width: 26px; height: 26px; flex: none; border-radius: 8px; background: var(--dg); color: var(--white); display: grid; place-items: center; }
.collage .gate-mark svg { width: 14px; height: 14px; fill: currentColor; }
.collage .gate-head b { font-size: 15px; font-weight: 500; color: var(--dg); }
/* the reply runs the full width of the phone, so its ticks sit beside the time rather than
   under it: site.css makes every svg a block, which is right for a short bubble that shrinks
   to its text and wrong for one that fills the screen. Scoped to this bubble, so every other
   bubble on the page keeps version A's rendering. */
.collage .c-phone .wide-bub time svg { display: inline-block; }
.collage .gate-read {
  display: flex; align-items: center; gap: 11px;
  margin: 10px; padding: 11px 13px;
  border: 1px solid transparent; border-radius: 14px;
}
.collage .gate-read + .gate-read { margin-top: 0; }
.collage .gate-icon { width: 24px; height: 24px; flex: none; border-radius: 50%; display: grid; place-items: center; }
.collage .gate-icon svg { width: 12px; height: 12px; fill: currentColor; }
.collage .gate-read b { font-size: var(--fs-sm); font-weight: 500; }
.collage .gate-read.is-in { background: var(--lg); color: var(--dg); }
.collage .gate-read.is-in .gate-icon { background: var(--dg); color: var(--white); }
/* The refusal. This palette has no red and is not getting one, so the door says no in the
   greys the page already sets its secondary type in, on its own --lightgray, inside a dashed
   ring that reads as a slot nothing was issued for. It is told apart from the read above it
   by weight and by movement rather than by hue, which is also why it survives being read by
   anyone who cannot tell the two grounds apart. */
.collage .gate-read.is-out { background: var(--lightgray); color: var(--gray2); border-style: dashed; border-color: var(--gray1); }
.collage .gate-read.is-out .gate-icon { background: var(--gray1); color: var(--gray2); }
.collage .staged .gate-read.is-out[data-t].shown { animation: refuse .52s var(--ease) both; }
/* a head shake, not a flash: the one movement on the page that means no */
@keyframes refuse {
  0% { transform: none; }
  18% { transform: translateX(-7px); }
  44% { transform: translateX(6px); }
  70% { transform: translateX(-3px); }
  100% { transform: none; }
}

/* --- Guest Desk, the arrivals ---
   A .stat-strip of one again, for the same reason the clock is one: climbStats and countUp
   already exist. The word beside it is the label this project puts on every illustrative
   figure in a mockup, at the size the rest of the page sets it. */
.collage .tally { grid-template-columns: 1fr auto; align-items: center; gap: 10px; }
.collage .tally > div { padding: 13px 15px; text-align: left; }
.collage .tally b {
  display: inline-block; min-width: 3ch;
  font-size: 40px; line-height: 1; color: var(--dg);
  font-variant-numeric: tabular-nums;
}
.collage .tally i { font-style: normal; margin-left: 8px; font-size: var(--fs-sm); color: var(--gray2); white-space: nowrap; }
.collage .tally span { display: block; margin-top: 7px; font-size: var(--fs-xs); color: var(--gray2); }
.collage .tally p {
  max-width: none; margin-right: 6px;
  font-size: var(--fs-micro); color: var(--gray2);
  border: 1px solid var(--gray1); border-radius: var(--r-full); padding: 3px 10px;
}

/* --- the line under the stage ---
   One caption per offer, because the three reels sell three different things and a single
   line under all of them would have to be true of none of them in particular. Only the
   active panel's line is in the flow; the other two are out of it entirely, so the caption
   never reserves room for a sentence it is not showing. */
.mock-caption .cap-set {
  flex: 1 1 100%;
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px 16px; flex-wrap: wrap;
}
.mock-caption .cap-set[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .collage .staged .plan-rows li[data-t].shown .plan-mark,
  .collage .staged .gate-read.is-out[data-t].shown { animation: none; }
}

/* below the collage breakpoint the cards stop being a scattered stage and stack in one 340px
   column, where the two display figures and the invitation's opening line are set larger than
   the column wants them */
@media (max-width: 760px) {
  .collage .timer b { font-size: 44px; }
  .collage .invite-card { padding: 26px 20px 24px; }
  .collage .invite-line.is-big { font-size: 23px; }
}

/* ---------- the hero headline's focus run ----------
   Every word blurred except the phrase in hand, four corner brackets standing off that phrase,
   three steps along the sentence and then a resolve that puts the headline back together. It
   plays once on load and stops. The JavaScript is in suite.js.

   Everything below is keyed on .tf-on, a class only the script adds, and the script only runs
   when the h1 carries data-focus. So the effect has two switches in series: no attribute, no
   effect; no script, no effect. In either case the headline is the plain sentence with the
   mint highlight, which is what the HTML on disk still says it is.

   The highlight steps aside rather than being removed: the mark keeps its .16em padding and
   its box-decoration-break, so the line breaks where it broke before and the headline takes
   the same number of lines with the effect on or off. Only the background colour goes, and it
   comes back on the last beat, so the run resolves into the finished headline rather than
   spending three steps dragging brackets across a mint block.

   The brackets are var(--btn), 2px, no glow. A coloured halo is the one thing that would make
   this read as a component someone dropped in rather than part of this page. */
.world-a .hero h1.tf-on {
  position: relative;
  /* The run. 320ms of travel between phrases, 400ms still on each of the three, and 340ms to
     resolve at the end, so the whole thing is over in a little over two seconds. suite.js
     carries the same three numbers, because it holds the clock and this holds the motion.
     The blur comes off a shade faster than the frame arrives, so a phrase is already sharp as
     the brackets land on it rather than clearing after them. --tf-in is the frame's own
     entrance, short because it is spending the first phrase's beat to do it. */
  --tf-travel: .5s;
  --tf-fade: .28s;
  --tf-in: .16s;
  --tf-out: .34s;
  --tf-blur: 4px;
  /* the frame: how far the arms stand off the phrase, how long they are, how thick. The arms
     are long and the standoff is wide because they are framing three or four words now, not
     one, and a small bracket on a phrase this size reads as a tick rather than a frame. The
     vertical standoff cannot match the horizontal one: the headline is set at line-height 1,
     so there is no space between the lines to stand off into. */
  --tf-gap-x: 16px;
  --tf-gap-y: 6px;
  --tf-arm: 28px;
  --tf-line: 2px;
  --tf-round: 4px;
}
/* no pointer cursor and no hover state anywhere below: nothing here is clickable, so nothing
   claims to be. The sentence stays selectable, as text should. */
.world-a .hero h1.tf-on .tf-word {
  filter: blur(var(--tf-blur));
  transition: filter var(--tf-fade) var(--ease);
}
.world-a .hero h1.tf-on .tf-word.is-focus { filter: blur(0); }
.world-a .hero h1.tf-on mark {
  background-color: transparent;
  transition: background-color var(--tf-out) var(--ease);
}

/* The frame is two absolutely positioned bands, each carrying two arms. The script writes each
   band's size and a translate, never its left and top, so the travel is one transform and two
   lengths a band rather than four lengths. A phrase that sits on one line puts both bands on
   the same rectangle and the four arms square up; a phrase that breaks across two lines, which
   only happens at the phone width, puts the top arms on the line it starts on and the bottom
   arms on the line it ends on, so the frame holds that phrase and not the words beside it. The
   whole thing is out of the flow and out of hit testing, so it cannot shift the headline or
   catch a click. */
.world-a .hero h1.tf-on .tf-frame {
  position: absolute; left: 0; top: 0; width: 0; height: 0;
  pointer-events: none; opacity: 0;
  transition: opacity var(--tf-in) var(--ease);
}
/* set once the first phrase has been measured, so the frame is never seen travelling out of
   the top left corner of the headline on load */
.world-a .hero h1.tf-on .tf-frame.is-ready { opacity: 1; }
.world-a .hero h1.tf-on .tf-band {
  position: absolute; left: 0; top: 0; width: 0; height: 0;
  transition: transform var(--tf-travel) var(--ease),
              width var(--tf-travel) var(--ease),
              height var(--tf-travel) var(--ease);
}
.world-a .hero h1.tf-on .tf-band i {
  position: absolute; width: var(--tf-arm); height: var(--tf-arm);
  border: var(--tf-line) solid var(--btn);
}
.world-a .hero h1.tf-on .tf-head i:nth-child(1) {
  top: calc(var(--tf-gap-y) * -1); left: calc(var(--tf-gap-x) * -1);
  border-right: 0; border-bottom: 0; border-top-left-radius: var(--tf-round);
}
.world-a .hero h1.tf-on .tf-head i:nth-child(2) {
  top: calc(var(--tf-gap-y) * -1); right: calc(var(--tf-gap-x) * -1);
  border-left: 0; border-bottom: 0; border-top-right-radius: var(--tf-round);
}
.world-a .hero h1.tf-on .tf-tail i:nth-child(1) {
  bottom: calc(var(--tf-gap-y) * -1); left: calc(var(--tf-gap-x) * -1);
  border-right: 0; border-top: 0; border-bottom-left-radius: var(--tf-round);
}
.world-a .hero h1.tf-on .tf-tail i:nth-child(2) {
  bottom: calc(var(--tf-gap-y) * -1); right: calc(var(--tf-gap-x) * -1);
  border-left: 0; border-top: 0; border-bottom-right-radius: var(--tf-round);
}

/* The resolve. One class the script adds after the frame has held "seconds." for its beat, and
   the three transitions declared above all run together on the same 340ms: every word goes
   sharp, the brackets fade out, the highlight fades in. What is left is the headline exactly as
   the page draws it with the effect switched off. */
.world-a .hero h1.tf-on.tf-done .tf-word {
  filter: blur(0);
  transition-duration: var(--tf-out);
}
.world-a .hero h1.tf-on.tf-done .tf-frame.is-ready {
  opacity: 0;
  transition-duration: var(--tf-out);
}
.world-a .hero h1.tf-on.tf-done mark { background-color: var(--lg); }

/* at the narrow end the headline is 48px rather than 64px, so the arms, the standoff and the
   blur all come in with it; four pixels of blur on 48px type is a heavier veil than the same
   four on 64px, and this is the width where the headline is read closest to the eye */
@media (max-width: 760px) {
  .world-a .hero h1.tf-on { --tf-arm: 20px; --tf-gap-x: 10px; --tf-gap-y: 5px; --tf-blur: 3px; }
}

/* Below about four hundred pixels the headline takes four lines instead of three, which is the
   one layout where a phrase breaks across lines and two phrases share a line: "in" ends one
   line with "seconds." beside it, and there are eleven pixels between them. The standoff has to
   live inside that gap, so it comes down to five and the arms come in to match. */
@media (max-width: 400px) {
  .world-a .hero h1.tf-on { --tf-arm: 17px; --tf-gap-x: 5px; --tf-gap-y: 4px; }
}

/* Blurred text is an accessibility problem, not a preference. Under reduced motion the script
   never builds the effect at all, so the headline is the plain sentence with its highlight;
   these three rules are the belt to that pair of braces. */
@media (prefers-reduced-motion: reduce) {
  .world-a .hero h1.tf-on .tf-word { filter: none; transition: none; }
  .world-a .hero h1.tf-on .tf-frame { display: none; }
  .world-a .hero h1.tf-on mark { background-color: var(--lg); transition: none; }
}

/* ---------- the tuner panel, dev only ----------
   Exists only when suite.js finds ?tune on the URL and builds it; without that, these rules
   simply have nothing in the page to match, which costs nothing. Deliberately not styled
   like the rest of the page, monospace and square and high contrast, so nobody mistakes it
   for part of the design. Fixed to the bottom left, clear of the WhatsApp button in the
   bottom right and clear of the hero above it, and capped so it can never push the page
   wider than the viewport at any size. */
.tf-tune, .tf-tune * { box-sizing: border-box; }
.tf-tune {
  position: fixed;
  left: 10px;
  bottom: 10px;
  z-index: 999999;
  width: 270px;
  max-width: calc(100vw - 20px);
  background: #111;
  color: #baffc9;
  border: 2px solid #baffc9;
  border-radius: 4px;
  padding: 10px 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.5;
  display: grid;
  gap: 8px;
}
.tf-tune p { margin: 0; }
.tf-tune-title { font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #fff; }
.tf-tune-row { display: grid; gap: 4px; }
.tf-tune-row span { display: flex; justify-content: space-between; gap: 8px; }
.tf-tune-row b, .tf-tune-total b { color: #fff; }
.tf-tune-row input[type="range"] { display: block; width: 100%; accent-color: #baffc9; }
.tf-tune-replay {
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  background: #baffc9;
  color: #111;
  border: 0;
  border-radius: 3px;
  padding: 8px 10px;
  cursor: pointer;
}
.tf-tune-replay:hover { background: #fff; }
.tf-tune-read { padding-top: 4px; border-top: 1px dashed #375d47; color: #fff; word-break: break-word; }


/* ---------- the caption under the hero collage ---------------------------------------- */
/* site.css gives .ticket a 1px --gray1 outline, which is right everywhere it is used as a
   chip. Under the collage it is not a chip, it is a caption, and its outline hangs in open
   space below the mockups where it reads as a stray grey line across the page. Onkar asked
   for that line twice. The outline goes, the words stay, and it now matches the plain line
   sitting beside it. Scoped to this one caption so every other .ticket on the page keeps
   its chip. */
.world-a .mock-caption .ticket {
  border: 0;
  padding-inline: 0;
  border-radius: 0;
  background: none;
}

/* site.css rules the logo strip off top and bottom. Both rules go, on all three suite
   pages. They are right on version A, where flat white means a hairline is the only thing
   marking either join. Here neither join needs one: the hero runs straight into the strip
   above it, and the cream trust row starts straight under it, so the change of ground is
   already doing the work and each rule reads instead as a stray grey line across the page.
   That is the spot Onkar has pointed at repeatedly, the top one first and now the bottom.
   version A is untouched and keeps both. */
.world-a .logos { border-top: 0; border-bottom: 0; }

/* ---------- the bottom edge blur ---------------------------------------------------------
   React Bits ships Gradual Blur as a React component. There is no React here and no build
   step, so what is ported is the technique, not the code: a stack of fixed bands across the
   foot of the viewport, each one carrying a stronger backdrop-filter than the one below it
   and a mask that limits it to its own fifth of the height. Where two masks overlap the
   blurs compound, which is what turns five steps into a ramp instead of five visible stairs.

   The numbers come straight from the component's defaults, position bottom, strength 2,
   height 6rem, five layers, linear curve, not animated. Its linear blur is
   0.0625 * (progress * count + 1) * strength, which for five layers and strength 2 gives
   0.25rem, 0.375rem, 0.5rem, 0.625rem and 0.75rem. Its mask for layer i runs
   transparent at (i-1)/5, opaque from i/5 to (i+1)/5, transparent again at (i+2)/5, with
   the stops past 100% dropped. Both sets are written out below rather than computed,
   because there is nothing here to compute them with and they never change.

   z-index 30 is the whole placement argument. The floating WhatsApp button sits at 40, the
   mobile menu at 49 and the nav at 50, so all three stay sharp and the button stays
   clickable through the band. Everything in the document flow is below 10, so the band
   covers the page and nothing else. The two dev tuning panels sit at 999999 and are
   likewise untouched. The WebGL layer on the bends and scanner pages is at -1, under the
   whole document, so it is part of what gets blurred rather than something that survives it.

   It can never take a click, a caret, a focus or a scroll. */
.edge-blur { display: none; }

/* Without backdrop-filter the band must leave no trace at all. A half-opaque strip across
   the foot of every page would read as a rendering fault rather than as a missing effect,
   so the element is display none by default and only a browser that can actually blur ever
   turns it on. */
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .edge-blur {
    display: block;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    height: 6rem;
    z-index: 30;
    pointer-events: none;
    -webkit-user-select: none;
    user-select: none;
  }
  .edge-blur span {
    display: block;
    position: absolute;
    inset: 0;
    pointer-events: none;
  }
  .edge-blur span:nth-child(1) {
    -webkit-backdrop-filter: blur(0.25rem);
    backdrop-filter: blur(0.25rem);
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 20%, #000 40%, transparent 60%);
    mask-image: linear-gradient(to bottom, transparent 0%, #000 20%, #000 40%, transparent 60%);
  }
  .edge-blur span:nth-child(2) {
    -webkit-backdrop-filter: blur(0.375rem);
    backdrop-filter: blur(0.375rem);
    -webkit-mask-image: linear-gradient(to bottom, transparent 20%, #000 40%, #000 60%, transparent 80%);
    mask-image: linear-gradient(to bottom, transparent 20%, #000 40%, #000 60%, transparent 80%);
  }
  .edge-blur span:nth-child(3) {
    -webkit-backdrop-filter: blur(0.5rem);
    backdrop-filter: blur(0.5rem);
    -webkit-mask-image: linear-gradient(to bottom, transparent 40%, #000 60%, #000 80%, transparent 100%);
    mask-image: linear-gradient(to bottom, transparent 40%, #000 60%, #000 80%, transparent 100%);
  }
  .edge-blur span:nth-child(4) {
    -webkit-backdrop-filter: blur(0.625rem);
    backdrop-filter: blur(0.625rem);
    -webkit-mask-image: linear-gradient(to bottom, transparent 60%, #000 80%, #000 100%);
    mask-image: linear-gradient(to bottom, transparent 60%, #000 80%, #000 100%);
  }
  .edge-blur span:nth-child(5) {
    -webkit-backdrop-filter: blur(0.75rem);
    backdrop-filter: blur(0.75rem);
    -webkit-mask-image: linear-gradient(to bottom, transparent 80%, #000 100%);
    mask-image: linear-gradient(to bottom, transparent 80%, #000 100%);
  }

  /* 6rem is a tenth of a 900 tall desktop window and a fifth of a phone held in landscape,
     and the band is a share of the height whatever the width says, so it is capped twice.
     At the page's own mobile breakpoint it comes down to 4.5rem, which at 390 by 844 still
     reads as a ramp and no longer swallows a whole chat bubble on its way off the screen.
     Below 520 of height, which is a phone on its side and a short desktop window, it comes
     down again to 3rem, because at that point a 6rem band is a quarter of everything the
     reader can see. The height rule is written second so it wins when both match. */
  /* The footer's legal row, the copyright and the two links, is the last thing on the page,
     so at the foot of the document it stood inside the band: smeared, still clickable, and
     taking a blurred focus ring with it. The band cannot be told to skip it, it blurs
     whatever is behind it, so the row is moved out from under it instead. The footer's bottom
     padding is the band's own height at each of the three sizes, which stands the row exactly
     on the band's top edge, where the mask has faded the blur to nothing. Not a pixel more:
     past that edge the band is falling on empty footer ground, where it cannot be seen at
     all. It is written inside the @supports with the band, because a browser without
     backdrop-filter draws no band and the footer keeps the 32px it closes on everywhere
     else. */
  .suite-footer { padding-bottom: 6rem; }

  @media (max-width: 760px) {
    .edge-blur { height: 4.5rem; }
    .suite-footer { padding-bottom: 4.5rem; }
  }
  @media (max-height: 520px) {
    .edge-blur { height: 3rem; }
    .suite-footer { padding-bottom: 3rem; }
  }
}


/* ---------- 16. the message costs dial ----------------------------------------------------

   The control at the foot of the receipt was a default range slider with a green track, and
   it read as a form field rather than as an instrument on the one section whose whole job is
   to be believed. It is a rotary dial now, in the style of a white volume knob, drawn in CSS
   and SVG so it is sharp at any size, recolours from the page's own tokens and costs nothing
   to download.

   The native <input type="range"> is still in the markup and is still the source of truth.
   site.js finds it by id on all six pages and does every piece of arithmetic and formatting;
   nothing here restates a rate or a figure. Under .js-dial the input is clipped to a 1px box
   that still takes focus and still carries every keyboard behaviour the browser gives a
   slider, and the dial writes into it. With JavaScript off, .js-dial is never added, .dial-row
   stays display:none, and the reader gets exactly the slider that shipped, styled by
   site.css, under exactly the label it had.

   Everything below is scoped to #costs or to .dial, because site.css styles range inputs with
   bare element selectors on all six pages and version-a.html is the chosen homepage. Nothing
   in this section may reach it.

   One more thing worth knowing before reading the rules: the dial takes ONE custom property
   write per pointer move, --t on .dial, and every lit dot and the indicator's angle fall out
   of it in calc(). No class toggling per frame and no per dot DOM work, which is what keeps a
   drag at one style resolution instead of a hundred. */

/* --t is the position, 0 at 1,000 messages and 1 at 100,000, and it inherits so the dots can
   read it. --i is the dot's own index and must NOT inherit or every dot would see the last
   one written. Registering them is only needed so --t can be transitioned: without @property
   the calc() substitution still works and the dial simply lands on its new value instantly,
   which is also exactly what reduced motion asks for, so the degradation is the feature. */
@property --t { syntax: '<number>'; inherits: true;  initial-value: 0; }
@property --i { syntax: '<number>'; inherits: false; initial-value: 0; }

/* The gate. Written as "hidden unless .js-dial" rather than "hidden by a class suite.js adds"
   so that a reader with no JavaScript, a failed script load or a blocked file never sees a
   half built dial with an empty dot ring. suite.js adds .js-dial to <html> only after it has
   found all four of its elements and drawn the dots. */
#costs .dial-row, #costs .dial-read { display: none; }
.js-dial #costs .dial-row { display: grid; grid-template-columns: 120px 1fr; gap: 18px; align-items: center; }
.js-dial #costs .dial-read { display: grid; justify-items: start; gap: 2px; }

/* The range is clipped rather than display:none or visibility:hidden, because both of those
   take it out of the tab order and out of the accessibility tree, and the whole design rests
   on it keeping its natural tab position and announcing itself. site.js also writes --fill
   onto this element every frame; at 1px with no children it inherits to nothing, so that
   write is inert and is left alone rather than guarded against. */
.js-dial #costs .receipt-foot > input[type="range"] {
  position: absolute; width: 1px; height: 1px; margin: 0; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
}
#costs .receipt-foot { position: relative; }

/* the head is what the old label element was: the name on the left, the live figure on the
   right. The figure moved OUT of the label on purpose. Inside it, the slider's accessible name
   was recomputed on every frame of a drag and read "Messages a month 24,000, slider, 24,000".
   As a sibling the name is a stable "Messages a month". site.js queries [data-out="n"] document
   wide, so all four readouts still update and site.js is not touched. */
#costs .dial-head { display: flex; justify-content: space-between; font-size: var(--fs-xs); color: var(--gray2); }
#costs .dial-head b { color: var(--dg); font-weight: 500; }

/* Every number the object is made of lives here, as a custom property, for two reasons. The
   geometry is expressed as a fraction of one size token --S, so there is one number to change
   when the dial is resized and nothing drifts out of proportion. And the shadow, gradient and
   colour values are properties rather than literals inside the box-shadow and background
   lists below, so the ?knob panel can write any of them live without this file being edited.
   The defaults here are the shipped values.

   --dial-shade is one warm ink, 96 84 64, for every shadow on the object, and it is
   deliberately NOT the codebase's green black. A knob is neutral plastic lit by room light,
   and a green shadow on neutral plastic reads as a tint rather than as shade.

   --dial-lit is lighter and less saturated than --green on purpose. #25D366 at full chroma
   against a warm white that moves through about seven per cent of luminance reads loud. The
   bloom carries the brand colour and the core reads as an emitter.

   --t's default is the markup's own value, 20,000 of 1,000 to 100,000, which is 19000/99000.
   Written out so the dial is drawn at the right angle on the very first frame, before
   suite.js has run, rather than swinging up from zero once it does. */
#costs .dial {
  position: relative; width: var(--S); height: var(--S); isolation: isolate; user-select: none;
  --S: 120px;
  /* Every shadow, rim and groove below is written at its 120px value and multiplied by this.
     Without it they are absolute pixels on an object whose size is a variable, so pushing --S
     up on the ?knob panel left a 24px blur on a 420px knob: the cast shadow stopped being a
     shadow and became a hard grey smear sitting under the collar rather than falling from it.

     It has to be a plain number, not calc(var(--S) / 120). That expression divides a length by
     a number and therefore yields a length, so every `calc(4px * var(--u))` below became px
     squared, which is not a length, and the whole declaration was thrown away. Length over
     length does give a number in Values 4, but not in enough shipped engines to rely on.

     So it is written by hand beside every --S: here, at the small breakpoint, and by the ?knob
     panel's size row. Three places, and the rule is simply --S divided by 120. */
  --u: 1;
  --wall: .055; --ring: .385; --dot: .018; --cap: .66;
  --n: 25;
  --t: .1919;                                   /* the markup's own default, 20,000 */
  --dial-shade: 96 84 64;                       /* one warm shadow ink for the whole object */
  --dial-lit: #3EDD7A; --dial-bloomc: #25D366; --dial-off: #E3DACE;
  --bloom: .55; --rim: 1.5px;
  /* the seating, the dome and the dots, pulled out of the lists below for the ?knob panel */
  --sh-x: 4px; --sh-y: 12px; --sh-blur: 24px;   /* the broad cast shadow */
  --sh-contact: .20;                            /* the tight one at the contact line */
  --ground: .14;                                /* occlusion on the cream around the base */
  --spec-x: 33%; --spec-y: 27%; --spec-s: 64%; --spec-a: .98;
  --sheen-a: .40;                               /* the second, softer lobe */
  --term: #EBE2D6;                              /* the warm terminator at the lower right */
  --rim-a: .96; --rim-from: 205deg;
  --bloom-blur: .018;                           /* as a fraction of --S, like everything else */
  --lip-a: .9;                                  /* the white crescent under an unlit dot */
  --amb: .11;                                   /* how much green the object throws on the cream */
  --mark-w: 3.5px; --mark-len: 19%; --mark-top: 9%;
  --mark-a: .40; --mark-a2: .24; --mark-lip: .92;
  --rise: .18s;
  /* the sweep, as two numbers rather than as literals, so the mark's angle and the ring of
     dots suite.js draws can never disagree about where the scale starts and how far it runs */
  --a0: -135; --sweep: 270;
  --on: calc(1 + var(--t) * (var(--n) - 1));
  --a: calc((var(--a0) + var(--t) * var(--sweep)) * 1deg);
}
/* The one transition that matters. It is on --t, not on the mark's transform, because --t is
   what the dots and the angle are both computed from, so one interpolation moves the whole
   object as one thing. It is off during a drag: a dial that eases towards the finger instead
   of sitting under it feels broken, and there is nothing to smooth because a pointer move is
   already one frame apart from the last one. */
#costs .dial:not(.is-drag) { transition: --t var(--rise) var(--ease); }

/* What the object throws on the room. Two radials behind everything: a green wash whose alpha
   rises with --t, so at 100,000 messages the cream around the dial is faintly green and the
   reader can see the control affecting its surroundings, and a plain white bloom that lifts
   the cream just enough to separate the collar's edge from it. One gradient stop of green is
   the cheapest detail here and the one most likely to sell the whole thing. */
#costs .dial::before {
  content: ''; position: absolute; inset: -26%; border-radius: 50%; z-index: -1; pointer-events: none;
  background:
    radial-gradient(closest-side, rgb(37 211 102 / calc(var(--t) * var(--amb))) 36%, transparent 74%),
    radial-gradient(closest-side, rgba(255,255,255,.36) 40%, rgba(255,255,255,.11) 62%, transparent 80%);
}
/* Ground occlusion, and it is a separate layer from the cast shadow on purpose. A cast shadow
   is light the object blocked; this is the darkening in the crease where the object meets the
   surface, which no blur of the cast shadow reproduces. Delete it and the knob looks printed
   on the cream rather than standing on it. */
#costs .dial::after {
  content: ''; position: absolute; inset: -3%; border-radius: 50%; z-index: -1; pointer-events: none;
  transform: translate(1px, 2px);
  background: radial-gradient(closest-side, transparent 87%, rgb(var(--dial-shade) / var(--ground)) 95%, transparent 100%);
}

/* The collar, the raised ring the cap sits inside. Three cast shadows rather than one, and
   the third is the one amateurs leave out: a broad soft shadow places the object in the room,
   a middle one fills the gap between the two scales, and a very tight dark one at the contact
   line, 1.5px down with 2px of blur and no spread, is what proves it is touching. Remove that
   third shadow and the whole knob floats off the page no matter what the other two do.
   --shadow-float is deliberately not reused here: it is tuned for a card lying flat and is
   badly wrong on a small round object with a short standoff. The three insets are the collar's
   own edge: a bright top lip, a dark bottom one and a hairline all the way round. */
#costs .dial-collar {
  position: absolute; inset: 0; border-radius: 50%;
  background: linear-gradient(148deg, #FFFFFF 0%, #FCF8F2 44%, #F4EDE2 78%, #EFE7DA 100%);
  box-shadow:
    calc(var(--sh-x) * var(--u)) calc(var(--sh-y) * var(--u)) calc(var(--sh-blur) * var(--u)) calc(-12px * var(--u)) rgb(var(--dial-shade) / .34),
    calc(2px * var(--u)) calc(5px * var(--u)) calc(10px * var(--u)) calc(-5px * var(--u)) rgb(var(--dial-shade) / .22),
    0 calc(1.5px * var(--u)) calc(2px * var(--u)) 0 rgb(var(--dial-shade) / var(--sh-contact)),
    inset 0 calc(1px * var(--u)) 0 rgba(255,255,255,.95),
    inset 0 calc(-1px * var(--u)) 0 rgb(var(--dial-shade) / .15),
    inset 0 0 0 calc(1px * var(--u)) rgba(255,255,255,.55);
}
/* the recessed face the dots are printed on. Its inset shadow is the wall of the collar
   falling inwards, which is why the dark one is at the top and the light one at the bottom:
   the opposite of the cap, because this surface faces the other way. */
#costs .dial-face {
  position: absolute; inset: calc(var(--S) * var(--wall)); border-radius: 50%;
  background: radial-gradient(104% 104% at 32% 26%, #FAF6F0 0%, #F4EEE5 60%, #EEE6D9 100%);
  box-shadow: inset 0 calc(2.5px * var(--u)) calc(5px * var(--u)) calc(-1px * var(--u)) rgb(var(--dial-shade) / .24),
              inset 0 calc(-1.5px * var(--u)) calc(3px * var(--u)) calc(-1px * var(--u)) rgba(255,255,255,.85);
}
/* pointer-events off so that every press lands on .dial-grip and the event target is never a
   circle element. There are a hundred of them and any one of them under a finger would be a
   target the drag maths has to cope with. */
#costs .dial-dots { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }

/* The dome, four stacked backgrounds and five shadows, and every one of them is doing a job.
   The first background is the specular, where the light lands. The second is a softer sheen
   offset from it, because a real specular on a curved surface is two lobed and without the
   second one the dome goes plastic flat. The third is the body, running white at the lit pole
   through neutral to a warm terminator at the lower right. The fourth is a flat base colour so
   no part of the cap is ever transparent over the face below it.

   The two alphas inside the specular are written as fractions of --spec-a rather than as their
   own numbers, so that dragging the specular's alpha on the ?knob panel moves the whole
   falloff together instead of flattening the highlight into a disc at one end of the slider.

   Then the shadows: a contact shadow proving the cap stands on the face, a second tighter one,
   a hairline groove along the top where the cap overhangs, an inset self occlusion at the
   lower right where the dome curls away from the light, and an inset bounce at the lower left
   where the cream panel throws light back up into the shadow side. That last one is the layer
   that gets omitted and the reason the dark side of this dome does not go dead. */
#costs .dial-cap {
  position: absolute; inset: calc(var(--S) * (1 - var(--cap)) / 2); border-radius: 50%;
  transform: translateY(var(--lift, 0px));
  transition: transform .16s var(--ease), box-shadow .16s var(--ease);
  background:
    radial-gradient(var(--spec-s) var(--spec-s) at var(--spec-x) var(--spec-y),
      rgb(255 255 255 / var(--spec-a)) 0%,
      rgb(255 255 255 / calc(var(--spec-a) * .633)) 30%,
      rgb(255 255 255 / calc(var(--spec-a) * .184)) 52%, transparent 70%),
    radial-gradient(90% 90% at 24% 44%, rgb(255 255 255 / var(--sheen-a)) 0%, transparent 58%),
    radial-gradient(118% 118% at 30% 24%, #FFFFFF 0%, #FBF7F2 40%, #F3ECE3 72%, var(--term) 100%),
    #F7F2EA;
  box-shadow:
    calc(2px * var(--u)) calc(5px * var(--u)) calc(11px * var(--u)) calc(-6px * var(--u)) rgb(var(--dial-shade) / .30),
    calc(1px * var(--u)) calc(2px * var(--u)) calc(4px * var(--u)) calc(-2px * var(--u)) rgb(var(--dial-shade) / .24),
    0 calc(-1px * var(--u)) 0 0 rgb(var(--dial-shade) / .12),
    inset calc(-6px * var(--u)) calc(-8px * var(--u)) calc(14px * var(--u)) calc(-9px * var(--u)) rgb(var(--dial-shade) / .38),
    inset calc(6px * var(--u)) calc(-5px * var(--u)) calc(13px * var(--u)) calc(-9px * var(--u)) rgba(255,252,246,.95);
}
/* The rim, a conic gradient masked to a ring of --rim thickness: brightest at the upper left
   where the light is, falling to nothing, then into occlusion at the lower right. Above about
   3px it stops reading as the lit edge of a curved object and starts reading as a stroked
   circle, which is why the default is 1.5px and why the panel's slider for it is short. The
   mask composite is what cuts the middle out: paint the padding box, subtract the content box,
   and what is left is the border. Prefixed first for older WebKit, unprefixed second so modern
   engines take the standard one.
   The two lower alphas are fractions of --rim-a for the same reason the specular's are. */
#costs .dial-cap::before {
  content: ''; position: absolute; inset: 0; border-radius: 50%; padding: calc(var(--rim) * var(--u)); pointer-events: none;
  background: conic-gradient(from var(--rim-from),
    rgba(255,255,255,0) 0deg,
    rgb(255 255 255 / var(--rim-a)) 58deg,
    rgb(255 255 255 / calc(var(--rim-a) * .542)) 124deg,
    rgba(255,255,255,0) 188deg,
    rgb(var(--dial-shade) / calc(var(--rim-a) * .229)) 252deg,
    rgba(255,255,255,0) 334deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}
/* The indicator, and the ONLY thing on the whole object that rotates. The cap, its rim, its
   gradients and the ring of dots are all bolted to the room, because a real knob's highlight
   is a reflection of the room and the room does not turn when the knob turns. Rotating the cap
   with its highlight attached is the fastest way to make this read as a CSS demo rather than
   as an object.

   It is a groove cut into the dome, not a line printed on it. A dark bar with a bright hairline
   directly below it, and that lip is the entire trick: a dark bar on its own is drawn on the
   surface, a dark bar with a bright lip under it is cut into it, because the light clears the
   near wall of the groove and lands on the far one. */
/* The groove, and the box it turns inside.

   That box is the CAP, not the dial. It was the dial, inset 0, and the consequence was that the
   groove began ten per cent down the whole object, which is above where the cap even starts: at
   420px the cut opened 29px clear of the cap, crossed the hairline that separates the cap from
   the face, and finished in the middle of the dot ring. It read as a stick laid across the knob
   rather than as something cut into it, which is exactly what it was. Insetting this box to the
   cap makes every figure below a fraction of the cap, so the cut is always inside the material
   it is cut from, at any size.

   Both boxes are concentric, so the rotation still turns about the same point it always did.

   The groove is wider and shorter than a drawn pointer would be. A line drawn on a surface can
   be as fine as you like. A cut in thick plastic has a width, because a mould has a width, and
   at 3.5px against a 79px cap that is about the proportion the reference carries. */
#costs .dial-mark {
  position: absolute;
  inset: calc(var(--S) * (1 - var(--cap)) / 2);
  border-radius: 50%;
  transform: rotate(var(--a));
  pointer-events: none;
  will-change: transform;
}
#costs .dial-mark::before {
  content: ''; position: absolute; left: 50%; top: var(--mark-top);
  width: calc(var(--mark-w) * var(--u)); height: var(--mark-len); transform: translateX(-50%);
  border-radius: var(--r-full);
  /* Dark at the top where the near wall of the cut shades itself, lighter at the bottom where
     the light that clears that wall lands on the far one. */
  background: linear-gradient(180deg, rgb(var(--dial-shade) / var(--mark-a)), rgb(var(--dial-shade) / var(--mark-a2)));
  /* And the lit lip on the far side of the cut, which is the whole difference between a line
     printed on the cap and a line cut into it. Scaled, or it stops being a hairline. */
  box-shadow: 0 calc(1px * var(--u)) 0 rgb(255 255 255 / var(--mark-lip)),
              inset 0 calc(1px * var(--u)) 0 rgb(255 255 255 / calc(var(--mark-lip) * .35));
}
/* One box takes every pointer event on the object. touch-action is none on THIS element only:
   on a 120px circle a vertical swipe should turn the dial rather than scroll the page, which
   is what a control is for, but any wider a hit area and it would be stealing scroll from the
   receipt rows above it. */
#costs .dial-grip { position: absolute; inset: 0; border-radius: 50%; cursor: grab; touch-action: none; }

/* The dots, drawn four times each because an LED is four things at once. The order in the
   markup is the painting order: bloom, lip, core, hot centre.

   --lit is the dot's own brightness, 0 to 1, computed from how far --on has passed its index.
   Because --t is passed through UNROUNDED, the leading dot brightens continuously as the value
   moves between detents. With 100 value steps and 25 dots, rounding --t would mean four
   consecutive arrow presses produce no visible change at all and the control would feel dead.
   This one line is what makes the dial feel connected to the number.

   The lip is the same dot offset 0.7 units downwards and fading out as the dot lights: a dark
   disc with a bright lower lip is a hole, which is what makes an unlit dot read as a recess
   rather than as a grey spot.

   The core is mixed in oklab, not sRGB. Mixing #E3DACE into #3EDD7A through sRGB passes
   through a muddy olive at the halfway point, which is exactly where the leading dot spends
   most of a drag. The plain fill on the line above the color-mix is a deliberate fallback for
   any engine without color-mix, and both lines stay.

   suite.js also writes r as an ATTRIBUTE on every circle it builds, as a fallback for engines
   that do not support r as a CSS property. */
#costs .dial-dots circle { --lit: clamp(0, calc(var(--on) - var(--i)), 1); }
#costs .dial-core circle, #costs .dial-lip circle { r: calc(var(--dot) * 200); }
#costs .dial-glow circle { r: calc(var(--dot) * 200 * 1.9); }
#costs .dial-hot   circle { r: calc(var(--dot) * 200 * .45); }
#costs .dial-glow { filter: blur(calc(var(--S) * var(--bloom-blur))); }
#costs .dial-glow circle { fill: var(--dial-bloomc); opacity: calc(var(--lit) * var(--bloom)); }
#costs .dial-lip  circle { fill: #FFFFFF; opacity: calc((1 - var(--lit)) * var(--lip-a)); }
#costs .dial-core circle { fill: var(--dial-off); fill: color-mix(in oklab, var(--dial-lit) calc(var(--lit) * 100%), var(--dial-off)); }
#costs .dial-hot  circle { fill: #FFFFFF; opacity: calc(var(--lit) * .5); }

/* The number is the input. At rest it has no border, no background and no chrome, and reads as
   typeset text beside the dial; it grows a field only on hover and focus. The width is a fixed
   7ch so "1,000" and "100,000" occupy the same box and the readout never twitches mid drag.
   font-variant-numeric is deliberately not declared: the field carries .num and site.css:142
   already gives it tabular figures, and the .receipt around it carries them too.
   Not green. Green is doing a job here as the lit dot colour, and a green number beside green
   dots is two greens competing for one meaning. */
#costs .dial-num {
  font: inherit; font-size: clamp(1.375rem, 1.25rem + .5vw, 1.625rem); font-weight: 500; line-height: 1.1;
  color: var(--dg); width: 7ch; text-align: left; background: transparent; border: 0;
  border-radius: var(--r-sm); padding: 2px 4px 3px;
  box-shadow: inset 0 -1px 0 transparent;
  transition: background-color .16s var(--ease), box-shadow .16s var(--ease);
}
#costs .dial-num:hover { background: rgba(255,255,255,.7); box-shadow: inset 0 -1px 0 var(--gray1); }
#costs .dial-num:focus-visible { background: var(--white); outline: 2px solid var(--focus); outline-offset: 2px; box-shadow: inset 0 -1px 0 transparent; }
/* what junk looks like. A red underline for 600ms, no shake, and the spoken line in
   #costs-say carries the same information for anyone who cannot see it. */
#costs .dial-num.is-rejected { box-shadow: inset 0 -1px 0 var(--red); }
#costs .dial-lbl { font-size: var(--fs-micro); font-weight: 500; letter-spacing: .12em; text-transform: uppercase; color: var(--gray2); }
#costs .dial-hint { font-size: var(--fs-micro); color: var(--gray2); margin: 0; }

/* Hover lifts the cap half a pixel and darkens the groove, which is all the affordance a knob
   needs. Pressing puts it back down half a pixel below where it started, on a shorter
   transition so it answers the finger, with a tighter shadow set and a deeper inset occlusion:
   a pressed object sits closer to what it stands on, so its shadow gets smaller and harder
   rather than softer. */
#costs .dial:hover { --lift: -.5px; --mark-a: .42; --mark-a2: .26; }
#costs .dial.is-drag { --lift: .5px; }
#costs .dial.is-drag .dial-cap {
  transition-duration: .1s;
  box-shadow:
    1px 2px 5px -4px rgb(var(--dial-shade) / .34),
    0 1px 2px -1px rgb(var(--dial-shade) / .26),
    0 -1px 0 0 rgb(var(--dial-shade) / .14),
    inset -7px -9px 15px -9px rgb(var(--dial-shade) / .46),
    inset  6px -5px 13px -9px rgba(255,252,246,.92);
}
#costs .dial.is-drag .dial-grip { cursor: grabbing; }

/* The focus ring is drawn on the collar, not on the 1px ghost the input has become, and it is
   reached with a sibling combinator rather than :has(). :has() would be the obvious tool and
   is the wrong one here: the sibling combinator works in every engine that has shipped in the
   last decade, and this is the only thing standing between a keyboard reader and a control
   with no visible focus at all. The input is the previous sibling of .dial-row in the markup,
   which is why the markup puts it there. */
.js-dial #costs #msgs:focus-visible ~ .dial-row .dial-collar { outline: 2px solid var(--focus); outline-offset: 4px; }

/* And it is taken off again for a press. onDown focuses the range on purpose, so that the wheel
   has something to be gated on and so that an arrow key works the instant the finger lifts.
   Chrome counts that programmatic focus on a form control as keyboard focus, so :focus-visible
   matches and the ring appeared on every click: a ring round a control the reader is already
   holding, which is noise. .is-tapped is written on the press and taken off by the first key,
   so the ring is back the moment the control is being driven from the keyboard, which is the
   only time it is doing any work. */
.js-dial #costs #msgs:focus-visible ~ .dial-row .dial.is-tapped .dial-collar { outline: 0; }

/* The figure is printed twice otherwise: once on the head row, where the slider used to need
   a readout beside its label, and once as the big number under the dial, which is also the
   field it is typed into. The head row's copy goes. It stays in the DOM rather than leaving
   the markup, because site.js writes all four [data-out="n"] nodes and the no JavaScript page
   still shows this one beside the native slider. */
.js-dial #costs .dial-head b { display: none; }

/* At 1440 the receipt column is about 504px and the foot's inner width about 460, so 120 plus
   an 18px gutter leaves about 320 for the number and its hint, which is comfortable. Below 560
   the whole panel is one column and that 320 has become about 200, so the dial gives up 16px
   rather than the number wrapping its hint on to three lines. --S is the only number that
   changes: every other part of the object is a fraction of it. */
@media (max-width: 560px) {
  .js-dial #costs .dial-row { grid-template-columns: 104px 1fr; gap: 14px; }
  #costs .dial { --S: 104px; --u: .867; }
}

/* Reduced motion takes away the --t transition, the cap's lift and press, and the field's
   hover fade. It does NOT take away the mark's rotation: the angle IS the value, not
   decoration, and a dial whose indicator does not move is not a dial. The value lands instead
   of sweeping, which is the whole of what was asked for.
   The first selector carries :not(.is-drag) only to match the specificity of the rule it is
   overriding above. Written as a plain #costs .dial it would lose, and reduced motion would
   silently keep the easing. */
@media (prefers-reduced-motion: reduce) {
  #costs .dial:not(.is-drag), #costs .dial-cap, #costs .dial-mark, #costs .dial-num { transition: none; }
}

/* ---------- the ?knob tuning panel, dev only ----------
   Exists only when suite.js finds ?knob on the URL and builds it; without that these rules
   have nothing in the page to match, which costs nothing. Every row writes one custom property
   on .dial, so what the panel changes is exactly what this file declares and there is never a
   second copy of a value to keep in step.
   Blue rather than the focus tuner's mint or the background tuner's amber, so that with more
   than one of them up it is never in doubt which panel is driving what. */
/* Docked bottom right, which is where Onkar asked for it.

   The background tuner in bg-bends.css docks in the same corner, so ?bg&knob together would
   put one on top of the other. The rule below lifts this panel clear of it when both are up:
   bg-bends.js writes .bgp on the page only when ?bg built it, so :has() finding one is exactly
   the condition, and an engine without :has() simply leaves the two stacked, which is a dev
   only inconvenience rather than a fault on the shipped page. */
.kbp, .kbp * { box-sizing: border-box; }
.kbp {
  position: fixed;
  right: 10px;
  bottom: 10px;
  z-index: 999999;
  width: 330px;
  max-width: calc(100vw - 20px);
  background: #111;
  color: #8fd8ff;
  border: 2px solid #8fd8ff;
  border-radius: 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11px;
  line-height: 1.45;
  display: grid;
  gap: 0;
}
.kbp-bar { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-bottom: 1px solid #2c4a5c; }
.kbp.is-shut { width: auto; right: 6px; bottom: 6px; }
.kbp.is-shut .kbp-bar { border-bottom: 0; }
.kbp.is-shut .kbp-title { display: none; }
.kbp.is-shut .kbp-body { display: none; }
.kbp-toggle {
  font: inherit; font-weight: 700; background: #8fd8ff; color: #111;
  border: 0; border-radius: 3px; padding: 3px 7px; cursor: pointer;
}
.kbp-toggle:hover { background: #fff; }
body:has(.bgp) .kbp { bottom: 420px; }
body:has(.bgp.is-shut) .kbp { bottom: 56px; }
.kbp-title { font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #fff; }
.kbp-body { max-height: min(58vh, 520px); overflow-y: auto; overscroll-behavior: contain; padding: 6px 8px 8px; display: grid; gap: 5px; }
.kbp-h { margin: 4px 0 0; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #fff; border-top: 1px dashed #2c4a5c; padding-top: 5px; }
.kbp-h:first-child { border-top: 0; margin-top: 0; padding-top: 0; }
.kbp-row { display: grid; gap: 2px; }
.kbp-row > span { display: flex; justify-content: space-between; gap: 8px; }
.kbp-row b { color: #fff; font-weight: 700; }
.kbp-row input[type="range"] { display: block; width: 100%; height: 14px; margin: 0; accent-color: #8fd8ff; }
.kbp-col { display: flex; align-items: center; gap: 6px; }
.kbp-col span { flex: 1 1 auto; }
.kbp-col input[type="color"] { width: 34px; height: 20px; padding: 0; border: 1px solid #2c4a5c; background: #111; cursor: pointer; }
.kbp-note { margin: 0; color: #5f93b0; }
.kbp-out { width: 100%; height: 150px; resize: vertical; background: #000; color: #8fd8ff; border: 1px solid #2c4a5c; border-radius: 3px; font: inherit; padding: 4px 5px; }
.kbp-btns { display: flex; gap: 6px; }
.kbp-btns button { font: inherit; font-weight: 700; background: #8fd8ff; color: #111; border: 0; border-radius: 3px; padding: 6px 9px; cursor: pointer; }
.kbp-btns button:hover { background: #fff; }
@media (max-width: 420px) {
  .kbp-body { max-height: 46vh; }
}

/* ---------- 17. the receipt itself ----------
   site.css draws this card and version A uses it unchanged, so nothing below edits that file:
   every rule here is scoped to #costs and simply outranks it on this page.

   What was wrong was not decoration. The card had five rows of identical weight, and only
   three of them are doing arithmetic. The first two are Meta's published rates, which are
   reference: they are the evidence for the sum, not part of it, and one of them, the utility
   rate, never enters the sum at all. The two that carry the whole argument, what a competitor
   adds and what we add, sat fourth and fifth in a flat list. A receipt's grammar is items,
   then a rule, then the thing you actually pay, and this one read as a table.

   So the rows are given three jobs instead of one: the rates are demoted to evidence, the
   charge is promoted to the line item, and the pair at the bottom is set against itself. No
   figure is added, moved or recalculated. Every number on this card is still the one site.js
   wrote there, which matters more here than anywhere else on the page. */

/* The head. It was tight enough that the title and the source ran into each other at 1024,
   where the column is at its narrowest before the panel goes single file. */
/* One warm ink for the whole section. It was declared on .dial only, which is a descendant of
   the foot's sibling, so the perforation's rgb(var(--dial-shade) / .30) resolved to nothing and
   the browser threw the entire background away: the holes were correct, measured, and not
   painted at all. Declared here it reaches both. The dial keeps its own copy of the same value
   so the ?knob panel can still move the object's shadows without moving the paper's. */
#costs { --dial-shade: 96 84 64; }

#costs .receipt-head { padding: 17px 22px; align-items: baseline; }
#costs .receipt-head b { font-size: var(--fs-sm); letter-spacing: -.005em; }
#costs .receipt-head span { font-size: var(--fs-micro); color: rgb(255 255 255 / .62); text-align: right; }

/* THE EVIDENCE, rows one and two. Published rates, quoted so the sum can be checked. Smaller,
   quieter, tighter, and on a faint tint so they read as a block of source material rather than
   as two more charges. The price keeps tabular figures and full contrast: it is being quoted
   as a fact, and a fact that is hard to read is not much of one. */
/* Two classes rather than :nth-of-type. That selector counts every sibling of the same tag,
   and .receipt-head is a div too, so the whole set was off by one and the line item's size
   landed on the utility rate. Positional selectors were the wrong tool anyway: these rows have
   different jobs, so they say so. */
#costs .receipt-row.rate { padding: 9px 22px; font-size: var(--fs-xs); background: rgb(16 57 40 / .022); }
#costs .receipt-row.rate:first-of-type { padding-top: 12px; }
#costs .receipt-row.rate + .rate { padding-bottom: 12px; border-top-color: rgb(16 57 40 / .05); }
#costs .receipt-row.rate span { color: var(--gray2); }
#costs .receipt-row.rate b { font-size: var(--fs-xs); }

/* THE LINE ITEM, row three. The only row on the card that is a charge you will actually pay,
   so it is the only one set at size. The rule above it is the receipt's fold: everything over
   it is evidence, everything under it is money. */
#costs .receipt-row.charge { padding: 16px 22px; border-top: 1px solid rgb(16 57 40 / .16); }
#costs .receipt-row.charge span { color: var(--dg); }
#costs .receipt-row.charge b { font-size: 1.125rem; }

/* THE PAIR. These two are the argument, and they only work against each other, so they are
   built as one object: a faint red ground for the charge you would be paying elsewhere, the
   mint ground already in site.css for the one you are not, and no rule between them, so the
   eye reads a single two line statement rather than two more entries in a list.

   The red is at .04, which is a tint and not a warning. A heavier one would read as an error
   on our own page, when the figure is a description of somebody else's pricing. */
#costs .receipt-row.add { background: rgb(234 0 56 / .04); border-top-color: rgb(234 0 56 / .10); }
#costs .receipt-row.add b { font-size: 1.0625rem; }
/* Both labels at the same weight and colour, so the only thing that differs across the pair is
   the figure and the ground it sits on. site.css greys every row's label, which is right for a
   list and wrong here: a grey question against a dark green answer reads as one of them being
   less certain than the other, when they are the same sentence said twice. */
#costs .receipt-row.add span, #costs .receipt-row.zero span { color: var(--dg); }
#costs .receipt-row.zero { border-top: 0; padding: 15px 22px; }
#costs .receipt-row.zero b { font-size: 1.0625rem; font-weight: 500; }

/* The foot is warm where the card is cool, which is deliberate: the dial is built in warm
   greys because a knob is neutral plastic under room light, and it needs a ground of its own
   family to sit on. What it did not have was a join. A hairline in the mint's own colour, so
   the change of temperature happens at a line rather than as a step. */
#costs .receipt-foot { border-top: 1px solid var(--lg2); padding: 16px 22px 20px; }
#costs .dial-head { margin-bottom: 2px; }

/* And the card itself sits a little deeper, because it now has more inside it. */
#costs .receipt { box-shadow: 0 26px 50px -28px rgb(16 57 40 / .5), 0 2px 4px -2px rgb(16 57 40 / .10); }

/* ---------- 17b. the receipt as a physical thing ----------
   The control below it is a knob: a moulded object with a dome, a cut groove and a shadow it
   sits in. A flat list of table rows above a knob is two different worlds stacked, and the
   knob wins, which is why the card read as plain the moment the dial went in.

   So the card becomes the other half of the same object. Paper printed by a machine, torn off
   along a perforation, with the machine's control panel underneath it. Nothing here is a
   texture or an ornament: the perforation is drawn on a join the card already had, where the
   white body ends and the warm panel begins. */

/* THE PERFORATION.

   Scallops were tried first and thrown out. Half discs of cream at a 9px pitch with a 4.2px
   radius leave six tenths of a pixel between them, so the edge reads as a straight line with
   slits in it rather than as a torn edge, which is what it looked like on screen. A row of
   punched holes is both more legible at this size and more like the thing it is imitating: a
   ticket is perforated, not scalloped.

   The holes are the mint of the row above showing through the panel below, which is what a
   hole is. 2px across on a 10px pitch, which at 100% is unmistakably a line of holes and at
   200% still is. */
#costs .receipt-foot::before {
  content: '';
  position: absolute;
  left: 14px; right: 14px; top: 0;
  height: 8px;
  background: radial-gradient(circle at 4.5px 4px, rgb(var(--dial-shade) / .30) 2.1px, transparent 2.2px) 0 0 / 9px 8px repeat-x;
  pointer-events: none;
}
/* The holes are the warm ink the dial's shadows are drawn in, not the mint of the row above.
   Mint on cream is a two per cent difference and the perforation simply disappeared: the holes
   were there, measured and photographed, and invisible at any normal size. A punched hole in
   paper reads as darker than the paper whatever is behind it, because you are looking into the
   thickness of the sheet. Using the dial's own shadow ink also means the tear and the knob are
   lit by the same lamp. */

/* the hairline join goes, because the perforation is the join now */
#costs .receipt-foot { border-top: 0; padding-top: 20px; }

/* DOTTED LEADERS, the way a printed statement runs a label out to its figure.

   On the two evidence rows only. The line item's label carries the live figure inside a nested
   span, so making that label a flex container puts a leader between the number and the words
   that belong to it, which is what it did on the first attempt. And the pair at the bottom is
   a sentence rather than an entry: a leader there would turn it back into a list. */
#costs .receipt-row.rate span { display: flex; align-items: baseline; gap: 9px; flex: 1 1 auto; min-width: 0; }
#costs .receipt-row.rate span::after {
  content: '';
  flex: 1 1 auto;
  min-width: 14px;
  align-self: center;
  border-bottom: 1px dotted rgb(16 57 40 / .26);
  transform: translateY(1px);
}

/* The punchline gets the room a punchline needs. It is the last line of the printed part, it
   is the only figure on the card that is ours, and it is the reason the section exists. */
#costs .receipt-row.zero { padding: 18px 22px 22px; }
#costs .receipt-row.zero span { font-weight: 500; }
#costs .receipt-row.zero b { font-size: 1.25rem; letter-spacing: -.01em; }
