/* =========================================================
   FOUR-PRODUCT PLATFORM (2026-09-18, owner steer)

   The homepage hero is now a diagram: Complly Construct at the
   centre, with Risk, FRA and Inspection streaming into it. The
   products section is one column of rows that slide in on scroll.

   Kept in its own file rather than appended to styles.css so the
   whole reposition can be reviewed — or reverted — in one place.

   Two structural notes worth keeping:

   1. The hero and the products section used to paint their own
      radial gradients, one under the other. That restarted the
      bright centre at the join and left a visible band, and the
      hero grid stopped dead on the same line. They now sit inside
      ONE .darkblock carrying a single gradient and a single masked
      grid, handing off cleanly to the white section below.

   2. The diagram SVG uses viewBox 1000x660 and .stage is locked to
      the SAME aspect ratio, so a circle at left:15%/top:32.6%
      lands exactly on SVG point (150,215). That is what keeps the
      streams meeting the circles at every width. Everything inside
      scales in cqw, so there is one layout, not a set of
      breakpoints.
   ========================================================= */

:root {
  /* per-product accents — all four are existing brand values, named
     once here so no rule has to carry a raw hex */
  --p-risk: var(--cyan);
  --p-construct: var(--blue);
  --p-fra: var(--amber);
  --p-inspect: var(--green);
}

/* ---------- one continuous dark block ---------- */
.darkblock { position: relative; overflow: hidden; background: radial-gradient(115% 62% at 50% -4%, #17335e 0%, var(--navy-900) 58%); }
.darkblock > * { position: relative; z-index: 1; }
.darkblock__bg { position: absolute; inset: 0; pointer-events: none; z-index: 0; }
.darkblock__grid {
  position: absolute; inset: 0;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.045) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.045) 1px, transparent 1px);
  background-size: 62px 62px;
  /* The grid holds a faint line ALL THE WAY DOWN (owner, 2026-09-19: "keep the faded lines all the
     way through to the bottom — it fades off as we transition between the footer and the page").
     It used to reach `transparent 78%`, so the last fifth of the block carried no grid at all and
     the hand-off to the footer read as an unfinished edge. Never returns to transparent now. */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.7) 55%, rgba(0, 0, 0, 0.42) 100%);
  mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.7) 55%, rgba(0, 0, 0, 0.42) 100%);
}
.darkblock .chero, .darkblock .archsec { background: none; }
.darkblock .archsec { overflow: visible; }
/* 🔑 A sitewide `.section` carries 130px top AND bottom — right between two UNRELATED sections on
   different backgrounds, wrong for two halves of one argument inside the SAME dark block. Measured
   at 1600: 210px between the Ask Rodney button and "Four products. One platform." with nothing in
   it (owner, 2026-09-19: "there's a gap there, can we make it smaller"). Tightened BY CLASS inside
   .darkblock only, so no other page's rhythm moves. Same call as #journey/#risk-journey on the apps
   page (2026-09-18). */
.darkblock .archsec { padding-top: clamp(40px, 4.4vw, 62px); }
.darkblock .archsec::before { display: none; }

/* ---------- the centred hero ---------- */
.chero { position: relative; padding: 144px 0 clamp(32px, 3.6vw, 52px); }
.chero__inner { position: relative; z-index: 2; text-align: center; }
.chero h1 { color: var(--white); font-size: clamp(31px, 4.1vw, 52px); margin-bottom: 14px; }
.chero__lede { font-size: clamp(15.5px, 1.2vw, 18px); color: var(--slate-300); max-width: 740px; margin: 0 auto; }
/* The "Ask Rodney" line that replaced the two hero buttons (owner, 2026-09-18). It is the old .askr
   band's headline brought up onto the dark hero — so white, not the slate it wore on a light band. */
.chero__ask { max-width: 700px; margin: clamp(22px, 2.6vw, 34px) auto 0; text-align: center; }
.chero__askline { font-family: var(--font-head); font-weight: 800; font-size: clamp(22px, 2.6vw, 32px); line-height: 1.15; letter-spacing: -0.025em; color: var(--white); margin: 0; }
.chero__askline span { background: linear-gradient(100deg, var(--blue) 0%, var(--cyan) 100%); -webkit-background-clip: text; background-clip: text; color: transparent; white-space: nowrap; }
.chero__asksub { margin: 12px auto 0; max-width: 620px; text-wrap: balance; font-size: clamp(14.5px, 1.1vw, 16px); line-height: 1.6; color: var(--slate-300); }
.chero__asksub b { color: var(--white); font-weight: 600; }
.chero__ask .hero__rodney { margin-top: 18px; }
@media (max-width: 900px) { .chero { padding-top: 124px; } }
@media (max-width: 680px) { .chero { padding-top: 104px; } }

/* ---------- the stage ---------- */
/* Owner steer 2026-09-18 — "nearer to where the white writing is, and less of a gap". The hub also
   moved up inside the stage, so this margin is the only remaining space between the lede and the
   badge; measured 25px before, 11px after. */
/* 🔑 THE DIAGRAM IS CAPPED IN **TWO** PLACES — .stagewrap AND .stage. Raise both or neither: an
   override on .stage alone does nothing, because the wrapper still clamps it. (Measured while
   chasing exactly that: setting .stage to 1160 left it at 735.) Same family as the pricing-dock
   trap on 2026-09-19 — when a rule has no effect, grep the stylesheet for the same property first.

   Owner, 2026-09-19: "I'm still seeing gaps on either side." Measured at 1600: the headline ran
   1272px wide and the diagram beneath it 735, leaving 433px of empty navy each side — and widening
   --maxw to 1320 had made that WORSE, because the headline grew with the container and the diagram
   did not. It is the diagram's own cap that matters here, not the page's.
   735 -> 1020 takes the gap to ~290 each side at 1600 and lets the diagram carry the section the way
   the headline above it does.
   ⚠️ `.stage` is aspect-ratio 1000/660, so width buys HEIGHT at 0.66x: this adds ~188px to the hero.
   That is why it is not wider still — the whole diagram has to stay above the fold. The apps page
   keeps its own 800px cap below (.astage), where the stage shares the column with nothing. */
.stagewrap { min-width: 0; max-width: 1020px; margin: clamp(4px, 0.7vw, 12px) auto 0; }
.stage {
  position: relative; container-type: inline-size;
  aspect-ratio: 1000 / 660; width: 100%; max-width: 1020px; margin: 0 auto;
}
/* 🔑 Width buys height here, so a SHORT screen has to get a smaller diagram or the bottom row of
   bubbles is cropped. Measured at 1600x1000: the bubbles end at 980. On a 900-tall viewport the
   same diagram would run 80px past the fold, so it steps down instead. Keyed on max-HEIGHT, which
   is the dimension that is actually short — a width query would shrink it on a wide-but-short
   screen for no reason, and leave it cropped on a narrow-but-tall one. */
@media (max-height: 940px) {
  .stagewrap, .stage { max-width: 860px; }
}
.stage__svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }

/* ---------- the hub ---------- */
.hub {
  /* Owner steer 2026-09-19 (mockup C, "Shallow V"): Construct sits in the CENTRE with FRA above it
     and Risk + Inspection out to the sides and a little below, so all three read as pouring upward
     into it. It replaces the 2026-09-18 arrangement (hub high, three feeders in a row beneath),
     which used only 87% of the stage width and left the sides empty — the gap the owner kept
     reporting. This fills 97%.
     🔑 The feeders' --nx/--ny live in index.html AND apps.html, and the SVG strand coordinates are
     GENERATED from the same numbers — change all of them together or the streams point into space.
     The generator (mockups/diagram/build.mjs — never committed) refuses a layout whose circles overlap
     or leave the canvas; three of my hand-tuned attempts did exactly that.
     🔑 The PHONE layout is a different arrangement entirely and does not read these values — see the
     max-width: 680px block, which sets `top`/`--mx` and swaps in the portrait SVG. */
  position: absolute; left: 50%; top: 53%; transform: translate(-50%, -50%);
  width: 27cqw; height: 27cqw; border-radius: 50%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.6cqw; text-align: center; padding: 4.2cqw 3cqw 3cqw; z-index: 4; cursor: pointer;
  background: radial-gradient(circle at 50% 32%, #1e3d6b 0%, #0d1c33 72%);
  border: 1.5px solid rgba(59, 130, 246, 0.55);
  box-shadow: 0 0 0 1.2cqw rgba(59, 130, 246, 0.07), 0 26px 60px -16px rgba(0, 0, 0, 0.85);
  transition: transform 0.35s var(--ease), border-color 0.3s var(--ease), box-shadow 0.35s var(--ease);
}
.hub::after {
  content: ""; position: absolute; inset: -1.5px; border-radius: 50%;
  border: 1px solid rgba(59, 130, 246, 0.5);
  animation: hubPulse 4s var(--ease) infinite;
}
@keyframes hubPulse {
  0% { transform: scale(1); opacity: 0.75; }
  70%, 100% { transform: scale(1.4); opacity: 0; }
}
.hub:hover, .hub:focus-visible {
  transform: translate(-50%, -50%) scale(1.04);
  border-color: var(--p-construct);
  box-shadow: 0 0 0 1.2cqw rgba(59, 130, 246, 0.17), 0 30px 66px -16px rgba(0, 0, 0, 0.9);
}
.hub:focus-visible { outline: 2px solid var(--white); outline-offset: 5px; }
.hub__name { font-family: var(--font-head); font-weight: 800; color: var(--white); font-size: 3.5cqw; line-height: 1.08; letter-spacing: -0.02em; }
.hub__name span { display: block; color: var(--p-construct); }
.hub__rod {
  display: flex; align-items: center; justify-content: center; gap: 0.7cqw;
  margin-top: 0.5cqw; padding-top: 0.8cqw; width: 86%;
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  font-family: var(--font-head); font-size: 1.3cqw; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--cyan);
}
.hub__rod svg { width: 1.9cqw; height: 1.9cqw; flex: none; }

/* The strapline badge sits ACROSS the hub upper edge, and must stay on TWO
   lines. On one line it is about 340 viewBox units wide and both upper
   streams pass straight through it; split, it is about 210 and they clear. */
/* 🔑 INSIDE the circle, not above it (2026-09-19). It used to hang off the hub's top edge, which is
   now where the FRA bubble and its strand are — they collided. Sitting inside also means it can no
   longer be clipped by the top of the stage. The phone block below still pins it above, because the
   portrait layout keeps the hub at the top with nothing over it. */
.hub__badge {
  position: static; margin-bottom: 0.7cqw;
  width: auto; max-width: 23cqw; padding: 0.95cqw 1.4cqw; border-radius: 1.5cqw;
  font-family: var(--font-head); font-size: 1.4cqw; font-weight: 700;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--cyan);
  line-height: 1.35; text-align: center;
  background: rgba(8, 17, 31, 0.96);
  border: 1px solid rgba(34, 211, 238, 0.5);
  box-shadow: 0 12px 28px -8px rgba(0, 0, 0, 0.9);
}
.hub:hover .hub__badge, .hub:focus-visible .hub__badge { border-color: rgba(34, 211, 238, 0.85); }

/* ---------- the three product circles ---------- */
.cnode {
  position: absolute; left: var(--nx); top: var(--ny); transform: translate(-50%, -50%);
  width: 19cqw; height: 19cqw; border-radius: 50%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.4cqw; padding: 2.4cqw; text-align: center; z-index: 4;
  background: radial-gradient(circle at 50% 30%, rgba(23, 44, 76, 0.96) 0%, rgba(12, 24, 40, 0.96) 74%);
  border: 1.5px solid color-mix(in srgb, var(--c) 42%, transparent);
  box-shadow: 0 0 0 0.9cqw color-mix(in srgb, var(--c) 7%, transparent), 0 20px 46px -16px rgba(0, 0, 0, 0.8);
  transition: transform 0.35s var(--ease), border-color 0.3s var(--ease), box-shadow 0.35s var(--ease), opacity 0.4s var(--ease);
}
.cnode--risk { --c: var(--p-risk); }
.cnode--construct { --c: var(--p-construct); }
.cnode--fra { --c: var(--p-fra); }
.cnode--inspect { --c: var(--p-inspect); }
.cnode:hover, .cnode:focus-visible {
  transform: translate(-50%, -50%) scale(1.045);
  border-color: var(--c);
  box-shadow: 0 0 0 0.9cqw color-mix(in srgb, var(--c) 16%, transparent), 0 26px 54px -16px rgba(0, 0, 0, 0.85);
  z-index: 6;
}
.cnode:focus-visible { outline: 2px solid var(--white); outline-offset: 4px; }
.cnode__ico { width: 3.2cqw; height: 3.2cqw; stroke: var(--c); fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.cnode__name { font-family: var(--font-head); font-weight: 800; color: var(--white); font-size: 2.6cqw; line-height: 1.08; letter-spacing: -0.02em; }
.cnode__name span { display: block; color: var(--c); }
.cnode__desc { font-size: 1.6cqw; line-height: 1.4; color: var(--slate-300); margin-top: 0.6cqw; }

/* ---------- the streams ---------- */
.haze { fill: none; stroke-width: 13; opacity: 0.13; stroke-linecap: round; }
.haze--risk { stroke: var(--p-risk); }
.haze--fra { stroke: var(--p-fra); }
.haze--inspect { stroke: var(--p-inspect); }

/* Six strands per spoke read as a flow rather than three dots. Each stream
   sits in its OWN <g>: nth-of-type counts every sibling <path>, so laid flat
   in the SVG the stagger silently breaks. */
.strand { fill: none; stroke-linecap: round; stroke-width: 2.4; opacity: 0.72; stroke-dasharray: 4 150; animation: strandIn 3.4s linear infinite; }
.strand--risk { stroke: var(--p-risk); }
.strand--fra { stroke: var(--p-fra); }
.strand--inspect { stroke: var(--p-inspect); }
.stream .strand:nth-of-type(1) { animation-delay: 0s; }
.stream .strand:nth-of-type(2) { animation-delay: -0.55s; }
.stream .strand:nth-of-type(3) { animation-delay: -1.1s; }
.stream .strand:nth-of-type(4) { animation-delay: -1.7s; }
.stream .strand:nth-of-type(5) { animation-delay: -2.25s; }
.stream .strand:nth-of-type(6) { animation-delay: -2.8s; }
@keyframes strandIn { from { stroke-dashoffset: 154; } to { stroke-dashoffset: 0; } }

/* Hovering a PRODUCT isolates it. Hovering the HUB lights everything, because
   the point of the hub is that it is where it all arrives — so nothing should
   recede. Two different states, two classes. */
.stage.is-lit .cnode { opacity: 0.34; }
.stage.is-lit .cnode:hover, .stage.is-lit .cnode:focus-visible { opacity: 1; }
.stage.is-lit .strand, .stage.is-lit .haze { opacity: 0.07; }
.stage.is-lit .strand.is-on { opacity: 0.85; }
.stage.is-lit .haze.is-on { opacity: 0.16; }
.stage.is-all .strand { opacity: 0.95; }
.stage.is-all .haze { opacity: 0.22; }
.stage.is-all .cnode { border-color: color-mix(in srgb, var(--c) 62%, transparent); }

/* ---------- the caption, and why it never resizes ----------
   Every line this slot can show is rendered into the SAME grid cell by
   platform.js: one visible layer plus a hidden ghost per variant. The slot is
   therefore as tall as the longest line it will ever hold, so the page never
   jumps when the text changes — correct at every width, which a min-height
   never is. */
.stagecap { max-width: 560px; margin: 8px auto 0; text-align: center; font-size: clamp(14.5px, 1.1vw, 16.5px); line-height: 1.5; color: var(--slate-400); transition: opacity 0.32s var(--ease); }
.stagecap b { color: var(--white); font-weight: 600; }
.stagecap.is-swap { opacity: 0; }
.stagecap__go { color: var(--cyan); font-weight: 600; }
.capname { display: block; font-family: var(--font-head); font-weight: 800; font-size: 1.08em; letter-spacing: -0.01em; line-height: 1.25; color: var(--white); margin-bottom: 3px; }
.capname--risk { color: var(--p-risk); }
.capname--construct { color: var(--p-construct); }
.capname--fra { color: var(--p-fra); }
.capname--inspect { color: var(--p-inspect); }
.swap { display: grid; }
.swap > * { grid-area: 1 / 1; }
.swap__ghost { visibility: hidden; pointer-events: none; user-select: none; }

/* ---------- the products list ---------- */
.plist { display: grid; gap: 0; max-width: 940px; margin: 0 auto; }
.prow { display: grid; grid-template-columns: 186px minmax(0, 1fr); gap: clamp(22px, 3vw, 44px); align-items: center; padding: clamp(24px, 3vw, 38px) 0; border-top: 1px solid rgba(255, 255, 255, 0.09); }
.prow:first-child { border-top: 0; }
.prow > .cnode { position: static; transform: none; width: 186px; height: 186px; padding: 22px; }
.prow > .cnode:hover, .prow > .cnode:focus-visible { transform: translateY(-4px); }
.prow > .cnode .cnode__ico { width: 28px; height: 28px; }
.prow > .cnode .cnode__name { font-size: 19px; }
.prow > .cnode .cnode__desc { font-size: 12px; }
.prow h3 { color: var(--white); font-size: clamp(22px, 2.3vw, 29px); margin-bottom: 7px; }
.prow h3 span { color: var(--c); }
.prow__who { font-size: 13.5px; color: var(--slate-400); margin-bottom: 11px; }
.prow__lead { font-size: 16px; color: var(--slate-300); line-height: 1.6; margin-bottom: 12px; }
.prow__tags { display: flex; flex-wrap: wrap; gap: 8px; }
.prow__tags span { font-family: var(--font-head); font-size: 12.5px; font-weight: 600; color: var(--slate-300); padding: 5px 12px; border-radius: 100px; border: 1px solid color-mix(in srgb, var(--c) 34%, transparent); background: color-mix(in srgb, var(--c) 9%, transparent); }
@media (max-width: 720px) {
  .prow { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .prow__tags { justify-content: center; }
}

/* ---------- slide in on scroll ----------
   Reversible on purpose: scroll back up and the row leaves again.

   Gated on `scripting: enabled` rather than the `.js` class an inline <head> script used to add —
   that class stopped arriving in the field and took every scroll animation on the site with it,
   silently. The full reasoning is on the matching .reveal rules in styles.css; the short version is
   that an inline script is interceptable and a media query is not. Keep the two in step: they are
   the same mechanism protecting the same thing. */
@media (scripting: enabled) {
  .slidein {
    opacity: 0;
    /* 110px, not 72 (owner 2026-09-18). 72px over 0.7s is a real move, but it is a small one
       against a full-width row — easy to miss entirely if you are scrolling rather than watching.
       A longer travel and a slightly slower settle make the direction legible, which is the point
       of alternating --dir in the first place. */
    transform: translateX(calc(var(--dir, -1) * 110px));
    transition: opacity 0.8s var(--ease), transform 0.8s var(--ease);
  }
  .slidein.is-in { opacity: 1; transform: none; }
}

/* ---------- narrow screens: the diagram stacks ---------- */
/* ON A PHONE THE DIAGRAM IS STILL A DIAGRAM (owner steer 2026-09-18: "how do we make the bubbles look
   more connected and flowing into the Construct bubble?"). It used to collapse into four circles stacked
   down the screen with the streams switched off — so the one idea the hero exists to show, three
   products FEEDING Construct, was exactly what a phone visitor never saw.

   Now the hub sits on top, the three feeders share a row beneath it, and a second, portrait SVG
   (.stage__svg--m, viewBox 340x430) carries the same animated strands up into the hub. The numbers
   below and that SVG's coordinates are ONE set: hub centre (170,105) r95, feeders at x 56/170/284,
   y 370, r50. Change them together or the streams point into space. The feeders drop their one-line
   descriptions here — at 100px there is no honest size to set them at — and keep icon + name. */
.stage__svg--m { display: none; }
@media (max-width: 680px) {
  .stage { aspect-ratio: 340 / 430; max-width: 340px; margin-top: 40px; }   /* headroom for the badge above the hub */
  .stage__svg { display: none; }
  .stage__svg--m { display: block; }
  .hub { top: 24.4%; width: 56cqw; height: 56cqw; padding: 6cqw 5cqw 5cqw; gap: 1.2cqw; }
  .hub__name { font-size: 7cqw; }
  /* "BACKED BY RODNEY" wrapped to two lines around the icon at the tracked-out desktop spacing;
     tighter tracking and a slightly smaller face keep it on one line inside the circle. */
  .hub__rod { font-size: 2.6cqw; letter-spacing: 0.04em; gap: 1.2cqw; padding-top: 2cqw; margin-top: 1cqw; white-space: nowrap; }
  .hub__rod svg { width: 4.2cqw; height: 4.2cqw; }
  /* The portrait layout keeps the hub at the TOP with nothing above it, so the badge goes back
     outside the circle. It needs `position` restored — the desktop rule now sets it to static. */
  .hub__badge { position: absolute; left: 50%; transform: translateX(-50%);
                top: -10.5cqw; margin-bottom: 0; width: 52cqw; max-width: none;
                font-size: 2.9cqw; padding: 2cqw 3cqw; border-radius: 3.4cqw; }
  .cnode { top: 86%; left: var(--mx, 50%); width: 29.5cqw; height: 29.5cqw; padding: 3cqw; gap: 1cqw; }
  .cnode--risk { --mx: 16.5%; } .cnode--fra { --mx: 50%; } .cnode--inspect { --mx: 83.5%; }
  .cnode__ico { width: 6cqw; height: 6cqw; }
  .cnode__name { font-size: 4.4cqw; }
  .cnode__desc { display: none; }
  .strand { stroke-width: 2; stroke-dasharray: 3 46; }   /* shorter runs: the same dash spacing would show one dot per stream */
  .haze { stroke-width: 9; }
}

@media (prefers-reduced-motion: reduce) {
  .strand { animation: none; opacity: 0.8; stroke-dasharray: none; }
  .hub::after { animation: none; }
  .slidein { opacity: 1; transform: none; transition: none; }
}

/* =========================================================
   ASK-RODNEY HOVER BOX — apps page hero (owner steer 2026-09-18)

   "A box at the top with Rodney — you hover over one of the
   different apps and it produces a different text message
   within the Rodney chat."

   It reads as Rodney speaking, so it borrows the chat bubble's
   vocabulary rather than inventing a new card: the brand mark,
   his name, a bot-side bubble. The four tabs carry the same
   per-product accents as the homepage diagram, so a colour
   means the same product on both pages.
   ========================================================= */
.askbox {
  max-width: 760px; margin: clamp(18px, 2.4vw, 30px) auto 0; padding: clamp(16px, 2vw, 22px);
  border-radius: 18px; text-align: left;
  background: rgba(10, 20, 36, 0.62);
  border: 1px solid rgba(148, 163, 184, 0.22);
  box-shadow: 0 22px 54px -22px rgba(0, 0, 0, 0.8);
  transition: border-color 0.3s var(--ease);
}
.askbox.is-lit { border-color: rgba(34, 211, 238, 0.42); }

.askbox__head { display: flex; align-items: center; gap: 9px; margin-bottom: 12px; }
.askbox__mark { width: 26px; height: 26px; flex: none; }
.askbox__mark svg { width: 100%; height: 100%; }
.askbox__who { font-family: var(--font-head); font-weight: 800; font-size: 15px; color: var(--white); letter-spacing: -0.01em; }
.askbox__hint { margin-left: auto; font-size: 12.5px; color: var(--slate-400); }

/* The bubble. The min-height here is only a floor for the no-JS case: appstage.js
   measures every message it can show and reserves the TALLEST, because the text
   is now typed in and a growing bubble would shove the tabs down line by line.
   A floor alone cannot do that - it is right at one width and wrong at the rest. */
.askbox__msg {
  margin: 0; padding: 13px 16px; border-radius: 14px 14px 14px 5px;
  background: rgba(255, 255, 255, 0.055);
  border: 1px solid rgba(148, 163, 184, 0.18);
  color: var(--slate-200); font-size: clamp(14.5px, 1.1vw, 16px); line-height: 1.55;
  min-height: 4.7em;
}
.askbox__msg b { color: var(--white); font-weight: 700; }

/* The cursor. It exists only while typing, so the bubble at rest is plain text
   and nothing blinks at a reader who is not hovering anything. */
.askbox__msg.is-typing::after {
  content: ''; display: inline-block;
  width: 2px; height: 1.05em; margin-left: 2px; vertical-align: -0.16em;
  background: var(--cyan);
  animation: askcaret 0.9s step-end infinite;
}
@keyframes askcaret { 0%, 55% { opacity: 1; } 56%, 100% { opacity: 0; } }

@media (max-width: 560px) {
  .askbox__hint { display: none; }          /* the head would wrap and push the bubble down */
  .askbox__msg { min-height: 6.5em; }       /* narrower column, so the same copy runs longer */
}

@media (prefers-reduced-motion: reduce) {
  .askbox { transition: none; }
  .askbox__msg.is-typing::after { display: none; }   /* appstage.js places the text at once here */
}

/* =========================================================
   THE APPS PAGE AS A STAGE (owner steer 2026-09-18)
   ---------------------------------------------------------
   apps.html used to be four product write-ups stacked down a
   long scroll. It is now one screen: Rodney on the left, the
   homepage's own four bubbles on the right. A bubble grows
   into a summary card; the write-ups live on /apps/<product>.

   The bubbles, strands and hub are the homepage components
   above, unchanged — only their size and surroundings differ.
   ========================================================= */

/* TIGHT, NOT TALL (owner, 2026-09-18: "make that a bit tighter, there's a lot of space on that page").
   The first version was min-height: 100vh with its content centred, which on a tall screen floated a
   550px composition in 900px of navy. It is now as tall as its content: the headline across the top,
   then Rodney and the diagram side by side, then straight into the two thread diagrams below. */
.astage { padding: 148px 0 clamp(20px, 2.6vw, 36px); }
.astage__head { text-align: center; margin-bottom: clamp(16px, 2.2vw, 30px); }
/* ONE CENTRED COLUMN (owner, 2026-09-19): headline, then the diagram, then Rodney beneath it.
   It was two columns with Rodney on the left, which pinned the diagram to 1.22fr of the row and
   left the box staring at it across a gap. Stacked, the diagram gets the full width — which is
   where the extra bubble size below comes from — and Rodney reads as a caption on it. */
.astage__grid {
  display: grid; justify-items: center;
  gap: clamp(20px, 2.6vw, 38px); text-align: center;
}
/* Spanning the page it sits on ONE line; in the left column it wrapped to two and pushed Rodney down. */
.astage h1 { font-size: clamp(34px, 4.6vw, 60px); line-height: 1.06; margin: 10px 0 0; color: var(--white); }
/* Pricing only: the one sentence that states the plan shape, under the headline. */
.astage__lede { max-width: 640px; margin: 14px auto 0; font-size: clamp(15px, 1.2vw, 17px); line-height: 1.6; color: var(--slate-300); }
.astage__lede b { color: var(--white); font-weight: 600; }
.astage .askbox { max-width: none; margin: 0; position: relative; z-index: 6; }
.astage__ask { margin-top: 18px; }

/* width:100% is load-bearing. Auto margins on a grid item switch off stretch, and a size-container
   has no intrinsic width to shrink to — without it the whole stage collapses to a dot. */
/* 🔑 BIGGER BUBBLES ARE ONE NUMBER, NOT FOUR (owner, 2026-09-19: "slightly increase the size of
   Complly Construct and the three bubbles"). The hub is 29cqw and each feeder 22cqw of `.stage`,
   which is a `container-type: inline-size` box, and the SVG strands are a viewBox — so widening the
   container scales the hub, the feeders, the text and the streams together, in proportion.
   Resizing .hub/.cnode directly would move the circles off the strand endpoints, which are
   generated from the same numbers in three places (see the note on .hub). 700 -> 800px: the hub goes
   203 -> 232px and each feeder 154 -> 176px. Kept to that because `.stage` is `aspect-ratio 1000/660`,
   so every px of width costs 0.66px of HEIGHT, and this composition is now vertical — 860px looked
   good in isolation and pushed Rodney a further 40px down the page. */
.astage .stagewrap { width: 100%; max-width: 920px; margin: 0 auto; }
.astage .stage { max-width: 920px; }
/* Rodney is a caption on the diagram now, so he is centred and no wider than it reads well at. */
.astage .astage__say { width: 100%; max-width: 620px; }
/* Bigger feeders than on the homepage: there the diagram sits under a headline, here it IS the page. */
/* 🔑 The apps page used to give its feeders 22cqw. Layout C cannot take that: measured, the FRA
   bubble at 22cqw hangs 11 units OFF THE TOP of the canvas and clears the hub by 6 (needs 18+).
   It uses the same 19cqw as the homepage now, and the stage is widened below instead — which keeps
   the bubbles the same size ON SCREEN (19cqw of 920 = 175px, where 22cqw of 800 was 176px) while
   giving both pages ONE set of numbers rather than a second layout to keep in step. */
/* The legacy anchors (#subcontractor, #fra…) live on the bubbles, and a browser scrolls an anchored
   element to the top of the viewport — measured: 565px, which dragged the whole hero up under the nav.
   A scroll margin taller than the viewport clamps that scroll to zero, so arriving by anchor lands on
   the page as designed and the card opens over it. */
.astage .hub, .astage .cnode { scroll-margin-top: 100vh; }
.astage .hub, .astage .cnode { cursor: pointer; transition-property: transform, border-color, box-shadow, opacity; }

/* The two thread diagrams under the stage. Sitewide a .section carries 130px top AND bottom, which is
   right between unrelated sections and wrong here: it put 130px of nothing under the hero and 260px
   between two diagrams that are one argument. Scoped by id so no other page's rhythm moves. */
#journey.journey, #risk-journey.journey { padding: clamp(40px, 4.6vw, 64px) 0; }
#risk-journey.journey { padding-top: clamp(8px, 1.4vw, 20px); padding-bottom: clamp(56px, 6vw, 88px); }

/* ---------- not sure where to start? ---------- */
.awho { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
.awho__lbl { width: 100%; font-size: 13px; color: var(--slate-400); letter-spacing: 0.04em; }
.awho__btn {
  padding: 9px 18px; border-radius: 999px; cursor: pointer;
  font-family: var(--font-head); font-weight: 700; font-size: 13.5px; color: var(--slate-200);
  background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(148, 163, 184, 0.28);
  transition: color 0.22s var(--ease), border-color 0.22s var(--ease), background 0.22s var(--ease);
}
.awho__btn:hover, .awho__btn[aria-pressed="true"] { color: var(--white); border-color: var(--cyan); background: rgba(34, 211, 238, 0.12); }
.awho__btn:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }

.stage.is-picking .hub:not(.is-pick), .stage.is-picking .cnode:not(.is-pick) { opacity: 0.32; }
.stage .is-pick { animation: astagePick 1.5s var(--ease) infinite; }
@keyframes astagePick {
  0%, 100% { box-shadow: 0 0 0 0.9cqw color-mix(in srgb, var(--c, var(--blue)) 10%, transparent), 0 20px 46px -16px rgba(0, 0, 0, 0.8); }
  50% { box-shadow: 0 0 0 2.6cqw color-mix(in srgb, var(--c, var(--blue)) 26%, transparent), 0 20px 46px -16px rgba(0, 0, 0, 0.8); }
}

/* ---------- the page takes the open product's colour ---------- */
.darkblock > .astage__tint {
  position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: 0;
  background: radial-gradient(90% 70% at 60% 45%, color-mix(in srgb, var(--c, var(--blue)) 34%, transparent) 0%, transparent 70%);
  transition: opacity 0.6s var(--ease);
}
.darkblock > .astage__tint.is-on { opacity: 1; }

/* The scrim sits INSIDE the hero section, not at body level. .darkblock gives each child its own
   stacking context, so nothing in the hero could be lifted above a body-level scrim — and Rodney
   (z-index 6, above) has to stay lit while a card is open, because he is narrating it. */
.astage__scrim {
  position: fixed; inset: 0; z-index: 5; background: rgba(5, 10, 20, 0.62);
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
  opacity: 0; pointer-events: none; transition: opacity 0.4s var(--ease);
}
.astage__scrim.is-on { opacity: 1; pointer-events: auto; }

/* ---------- the card a bubble grows into ----------
   ONE element that starts laid exactly over the bubble (same box, 50% radius) and is transitioned
   to its resting box — so the bubble BECOMES the card; nothing fades in over the top of it.
   left/top/width/height are animated, not a transform scale: scaling a circle into a rectangle
   stretches the corner radius into an ellipse on the way. It is one element, once per click. */
.acard {
  --c: var(--p-construct);
  position: fixed; z-index: 95; overflow: hidden; text-align: left;   /* under the nav (100): it opens below it */
  background: radial-gradient(circle at 50% 0%, #1b3760 0%, #0b1729 62%);
  border: 1.5px solid color-mix(in srgb, var(--c) 62%, transparent);
  box-shadow: 0 0 0 10px color-mix(in srgb, var(--c) 8%, transparent), 0 40px 90px -20px rgba(0, 0, 0, 0.9);
}
.acard[hidden] { display: none; }
/* The floating Ask-Rodney launcher sits bottom-right, which is where the card's corner lands. It steps
   aside while a card is open — the in-page Ask Rodney button beside the card is still there. */
body.is-acard-open .rodney__launch { opacity: 0; pointer-events: none; transition: opacity 0.25s var(--ease); }
.acard.is-moving {
  transition: left 0.52s var(--ease), top 0.52s var(--ease), width 0.52s var(--ease),
              height 0.52s var(--ease), border-radius 0.52s var(--ease);
}
.acard__in { padding: clamp(22px, 3vw, 34px); opacity: 0; transform: translateY(8px); transition: opacity 0.28s var(--ease), transform 0.28s var(--ease); }
.acard.is-open .acard__in { opacity: 1; transform: none; }
/* Only a card that had to be clamped to a short viewport scrolls — otherwise a half-pixel of
   rounding grows a scrollbar the card does not need. */
.acard.is-clamped .acard__in { overflow-y: auto; max-height: 100%; }

.acard__x {
  position: absolute; right: 14px; top: 14px; width: 36px; height: 36px; border-radius: 50%; z-index: 2;
  font-size: 20px; line-height: 1; color: var(--slate-300); cursor: pointer;
  background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(148, 163, 184, 0.25);
  opacity: 0; transition: opacity 0.28s var(--ease), color 0.2s var(--ease), border-color 0.2s var(--ease);
}
.acard.is-open .acard__x { opacity: 1; }
.acard__x:hover { color: var(--white); border-color: var(--c); }
.acard__x:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }

.acard__head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; padding-right: 44px; }
.acard__ico {
  width: 40px; height: 40px; flex: none; border-radius: 50%; display: grid; place-items: center;
  background: color-mix(in srgb, var(--c) 14%, transparent); border: 1px solid color-mix(in srgb, var(--c) 50%, transparent);
}
.acard__ico svg { width: 20px; height: 20px; stroke: var(--c); fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.acard__name { font-family: var(--font-head); font-weight: 800; font-size: 21px; line-height: 1.1; color: var(--white); letter-spacing: -0.02em; }
.acard__name span { color: var(--c); }
.acard__for { font-family: var(--font-head); font-weight: 700; font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--c); margin-bottom: 6px; }
.acard h2 { font-size: clamp(21px, 2.3vw, 27px); line-height: 1.18; color: var(--white); margin: 0 0 10px; }
.acard__lede { font-size: 15px; line-height: 1.6; color: var(--slate-300); margin-bottom: 16px; }

.acard__caps { list-style: none; display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin: 0 0 16px; padding: 0; }
.acard__caps li {
  padding: 11px 13px; border-radius: 12px; font-size: 13.5px; line-height: 1.45; color: var(--slate-200);
  background: rgba(255, 255, 255, 0.045); border: 1px solid rgba(148, 163, 184, 0.16);
  opacity: 0; transform: translateY(10px);
}
.acard__caps b { display: block; color: var(--white); font-family: var(--font-head); font-weight: 700; margin-bottom: 2px; }
/* The four capabilities arrive one after another, so the eye is led down the card. */
.acard.is-open .acard__caps li { opacity: 1; transform: none; transition: opacity 0.4s var(--ease), transform 0.4s var(--ease); }
.acard.is-open .acard__caps li:nth-child(1) { transition-delay: 0.10s; }
.acard.is-open .acard__caps li:nth-child(2) { transition-delay: 0.17s; }
.acard.is-open .acard__caps li:nth-child(3) { transition-delay: 0.24s; }
.acard.is-open .acard__caps li:nth-child(4) { transition-delay: 0.31s; }

.acard__feeds {
  font-size: 13px; line-height: 1.5; color: var(--slate-400); padding: 10px 13px; margin-bottom: 18px;
  border-left: 2px solid var(--c); background: rgba(255, 255, 255, 0.025); border-radius: 0 10px 10px 0;
}
.acard__feeds b { color: var(--slate-200); font-weight: 600; }
.acard__act { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 18px; }
.acard__more { font-family: var(--font-head); font-weight: 700; font-size: 14.5px; color: var(--c); text-decoration: none; }
.acard__more:hover, .acard__more:focus-visible { text-decoration: underline; }

@media (max-width: 1000px) {
  .astage { padding-top: 146px; }
  /* (the grid is one centred column at every width now — nothing to restack here) */
  /* Stacked, the card covers Rodney anyway — so he goes under the scrim with everything else rather
     than poking out lit from behind the card. */
  .astage .askbox { z-index: auto; }
  .awho { justify-content: center; }
}
@media (max-width: 560px) { .acard__caps { grid-template-columns: 1fr; } }

@media (prefers-reduced-motion: reduce) {
  .acard.is-moving, .acard__in, .acard__caps li, .acard__x,
  .darkblock > .astage__tint, .astage__scrim, .awho__btn { transition: none !important; }
  .stage .is-pick { animation: none; }
}

/* =========================================================
   PRODUCT PAGES — /apps/<product>.html
   Generated by scripts/build-apps.mjs. The body of each page is
   the long write-up that used to sit on apps.html, unchanged,
   so it keeps the .journey / .apps / .offer styles it always had.
   ========================================================= */
.phero { --c: var(--p-construct); padding: 150px 0 50px; position: relative; }
.phero--risk { --c: var(--p-risk); } .phero--fra { --c: var(--p-fra); } .phero--inspect { --c: var(--p-inspect); }
.phero::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(70% 90% at 22% 40%, color-mix(in srgb, var(--c) 24%, transparent), transparent 70%);
}
.phero .container { position: relative; }
.pcrumb { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 13.5px; color: var(--slate-400); margin-bottom: 28px; }
.pcrumb a { color: var(--slate-300); text-decoration: none; }
.pcrumb a:hover, .pcrumb a:focus-visible { color: var(--white); text-decoration: underline; }
.pcrumb b { color: var(--white); font-weight: 600; }

.phero__grid { display: grid; grid-template-columns: auto 1fr; gap: clamp(22px, 4vw, 54px); align-items: center; }
.pbubble {
  width: clamp(150px, 19vw, 230px); aspect-ratio: 1; border-radius: 50%; position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 8px;
  background: radial-gradient(circle at 50% 32%, #1e3d6b 0%, #0d1c33 72%);
  border: 1.5px solid var(--c);
  box-shadow: 0 0 0 12px color-mix(in srgb, var(--c) 9%, transparent), 0 26px 60px -16px rgba(0, 0, 0, 0.85);
}
.pbubble::after {
  content: ""; position: absolute; inset: -1.5px; border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--c) 60%, transparent); animation: hubPulse 4s var(--ease) infinite;
}
.pbubble svg { width: 30px; height: 30px; stroke: var(--c); fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.pbubble__name { font-family: var(--font-head); font-weight: 800; font-size: clamp(19px, 2.2vw, 27px); line-height: 1.08; color: var(--white); letter-spacing: -0.02em; }
.pbubble__name span { display: block; color: var(--c); }

.phero__for { font-family: var(--font-head); font-weight: 700; font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--c); }
.phero h1 { font-size: clamp(30px, 4vw, 50px); line-height: 1.1; color: var(--white); margin: 8px 0 14px; }
.phero__lede { font-size: clamp(15.5px, 1.25vw, 18px); line-height: 1.6; color: var(--slate-300); max-width: 660px; }
.phero__act { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 22px; }
.phero__soon { margin-top: 14px; font-size: 13.5px; color: var(--slate-400); }

/* ---------- sideways between products, without going back ---------- */
.pfeed { padding: 56px 0 72px; text-align: center; background: var(--navy-900); border-top: 1px solid rgba(148, 163, 184, 0.14); }
.pfeed h2 { font-size: clamp(22px, 2.4vw, 30px); color: var(--white); margin: 0 0 6px; }
.pfeed p { color: var(--slate-400); font-size: 15px; margin-bottom: 28px; }
.pfeed__row { display: flex; flex-wrap: wrap; justify-content: center; gap: clamp(16px, 3vw, 40px); }
.pfeed__b {
  --c: var(--cyan); width: 172px; aspect-ratio: 1; border-radius: 50%; text-decoration: none;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; padding: 18px; text-align: center;
  background: radial-gradient(circle at 50% 30%, rgba(23, 44, 76, 0.96), rgba(12, 24, 40, 0.96) 74%);
  border: 1.5px solid color-mix(in srgb, var(--c) 42%, transparent);
  transition: transform 0.35s var(--ease), border-color 0.3s var(--ease), box-shadow 0.35s var(--ease);
}
.pfeed__b:hover, .pfeed__b:focus-visible { transform: scale(1.06); border-color: var(--c); box-shadow: 0 0 0 9px color-mix(in srgb, var(--c) 14%, transparent); }
.pfeed__b:focus-visible { outline: 2px solid var(--white); outline-offset: 4px; }
.pfeed__b--construct { --c: var(--p-construct); } .pfeed__b--risk { --c: var(--p-risk); }
.pfeed__b--fra { --c: var(--p-fra); } .pfeed__b--inspect { --c: var(--p-inspect); }
.pfeed__b b { font-family: var(--font-head); font-weight: 800; font-size: 17px; line-height: 1.1; color: var(--white); }
.pfeed__b b span { display: block; color: var(--c); }
.pfeed__b small { font-size: 11.5px; line-height: 1.35; color: var(--slate-300); }
.pfeed__b em { font-family: var(--font-head); font-weight: 700; font-size: 10px; font-style: normal; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c); margin-top: 3px; }

@media (max-width: 760px) {
  .phero__grid { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .phero__lede { margin: 0 auto; }
  .phero__act, .pcrumb { justify-content: center; }
}
@media (prefers-reduced-motion: reduce) {
  .pbubble::after { animation: none; }
  .pfeed__b { transition: none; }
}

/* =========================================================
   PRICING — four bubbles in a row, one price panel beneath
   (owner steer 2026-09-18, second pass; assets/js/pricetabs.js)
   ---------------------------------------------------------
   data-plan on the section is the ONLY switch: it sets --c, and
   the lit bubble, its stem, the panel edge, the price slot and the
   background glow all read --c. One attribute, so the colour is
   never half-applied.
   ========================================================= */
.pplans { --c: var(--p-construct); position: relative; padding: 150px 0 clamp(48px, 6vw, 84px); }
.pplans[data-plan="risk"] { --c: var(--p-risk); }
.pplans[data-plan="fra"] { --c: var(--p-fra); }
.pplans[data-plan="inspect"] { --c: var(--p-inspect); }
.pplans::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(60% 55% at 50% 58%, color-mix(in srgb, var(--c) 17%, transparent), transparent 70%);
  transition: background 0.6s var(--ease);
}
.pplans > .container { position: relative; }
.pplans__head { text-align: center; }
.pplans__head h1 { font-size: clamp(34px, 4.6vw, 60px); line-height: 1.06; color: var(--white); margin: 0; }
.pplans__stage { max-width: 940px; margin: clamp(26px, 3.2vw, 44px) auto 0; }

.pplans__row { display: grid; grid-template-columns: repeat(4, 1fr); justify-items: center; }
.pbub {
  --b: var(--p-construct);
  position: relative; width: clamp(128px, 15vw, 168px); aspect-ratio: 1; border-radius: 50%; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; padding: 14px;
  font: inherit; color: inherit; text-align: center;
  background: radial-gradient(circle at 50% 30%, rgba(23, 44, 76, 0.96) 0%, rgba(12, 24, 40, 0.96) 74%);
  border: 1.5px solid color-mix(in srgb, var(--b) 40%, transparent);
  box-shadow: 0 0 0 8px color-mix(in srgb, var(--b) 6%, transparent), 0 20px 46px -16px rgba(0, 0, 0, 0.8);
  opacity: 0.62;
  transition: transform 0.4s var(--ease), border-color 0.3s var(--ease), box-shadow 0.4s var(--ease), opacity 0.3s var(--ease);
}
.pbub--construct { --b: var(--p-construct); } .pbub--risk { --b: var(--p-risk); }
.pbub--fra { --b: var(--p-fra); } .pbub--inspect { --b: var(--p-inspect); }
.pbub:hover, .pbub:focus-visible { opacity: 1; transform: translateY(-4px); border-color: var(--b); }
.pbub:focus-visible { outline: 2px solid var(--white); outline-offset: 5px; }
.pbub[aria-selected="true"] {
  opacity: 1; transform: translateY(-6px) scale(1.06); border-color: var(--b);
  box-shadow: 0 0 0 10px color-mix(in srgb, var(--b) 15%, transparent), 0 0 46px -6px var(--b), 0 26px 54px -16px rgba(0, 0, 0, 0.85);
}
.pbub__ico { width: 24px; height: 24px; stroke: var(--b); fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.pbub__name { font-family: var(--font-head); font-weight: 800; font-size: clamp(15px, 1.5vw, 19px); line-height: 1.08; color: var(--white); letter-spacing: -0.02em; }
.pbub__name span { display: block; color: var(--b); }
.pbub__tag { font-family: var(--font-head); font-weight: 700; font-size: 9.5px; letter-spacing: 0.09em; text-transform: uppercase; color: var(--slate-300); }

/* The stem: the lit bubble pours down into the panel. It only exists once the script has taken over
   (.is-tabbed is added to the dock, and the row precedes it), so an un-scripted page shows no stray line. */
.pbub::after {
  content: ""; position: absolute; left: 50%; top: 100%; width: 2px; height: 0; transform: translateX(-50%);
  background: linear-gradient(180deg, var(--b), color-mix(in srgb, var(--b) 25%, transparent));
  transition: height 0.4s var(--ease) 0.08s;
}
.pbub[aria-selected="true"]::after { height: clamp(34px, 4vw, 52px); }

/* ONE HEIGHT FOR ALL FOUR (owner, 2026-09-19: "I want it to all be the same size box").
   The panels differ by 59px — Construct/Risk measure 382, FRA/Inspection 441 — so switching product
   resized the dock and shoved the footer up and down under the pointer. All four now sit in the SAME
   grid cell, so the dock is always as tall as the tallest and NOTHING below it moves.

   The unselected ones keep their [hidden] attribute (the script sets it, and it is what says which
   panel is current) but are painted with visibility instead of display: visibility:hidden still
   removes them from the accessibility tree and from the tab order, while KEEPING their box — which
   display:none, the UA default for [hidden], is exactly what does not do. */
.pplans__dock { margin-top: clamp(30px, 3.6vw, 46px); display: grid; }
.pplans__dock > .ppanel { grid-area: 1 / 1; }
.ppanel[hidden] { display: grid; visibility: hidden; pointer-events: none; }
.ppanel {
  --c: var(--p-construct);
  display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); border-radius: 26px; overflow: hidden;
  background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--c) 16%, #0d1b31) 0%, #0b1729 58%);
  border: 1.5px solid color-mix(in srgb, var(--c) 55%, transparent);
  box-shadow: 0 0 0 10px color-mix(in srgb, var(--c) 6%, transparent), 0 40px 90px -30px rgba(0, 0, 0, 0.9);
}
.ppanel--risk { --c: var(--p-risk); } .ppanel--fra { --c: var(--p-fra); } .ppanel--inspect { --c: var(--p-inspect); }
.ppanel:focus-visible { outline: 2px solid var(--white); outline-offset: 4px; }
.ppanel.is-in { animation: ppanelIn 0.5s var(--ease) both; }
@keyframes ppanelIn { from { opacity: 0; transform: translateY(-14px) scale(0.985); } to { opacity: 1; transform: none; } }

.ppanel__price { padding: clamp(24px, 3.2vw, 40px); }
.ppanel__pkg { font-family: var(--font-head); font-weight: 700; font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--c); }
.ppanel__name { font-size: clamp(20px, 2vw, 25px); color: var(--white); margin: 6px 0 0; letter-spacing: -0.02em; }
.ppanel__name span { color: var(--c); }
/* THE PRICE SLOT. "Coming soon" today; the day pricing launches the figure goes here and nothing else moves. */
.ppanel__figure {
  margin: clamp(14px, 1.8vw, 22px) 0 0; font-family: var(--font-head); font-weight: 800; letter-spacing: -0.035em;
  font-size: clamp(38px, 5vw, 60px); line-height: 1;
  background: linear-gradient(100deg, var(--white) 0%, var(--c) 100%); -webkit-background-clip: text; background-clip: text; color: transparent;
}
.ppanel__unit { margin-top: 10px; font-family: var(--font-head); font-weight: 700; font-size: 15px; color: var(--slate-200); }
.ppanel__fine { margin-top: 8px; font-size: 13.5px; line-height: 1.55; color: var(--slate-400); max-width: 420px; }
.ppanel__act { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; margin-top: clamp(18px, 2.2vw, 26px); }
.ppanel__more { font-family: var(--font-head); font-weight: 700; font-size: 14.5px; color: var(--c); }
.ppanel__more:hover, .ppanel__more:focus-visible { text-decoration: underline; }

.ppanel__in { padding: clamp(24px, 3.2vw, 40px); background: rgba(255, 255, 255, 0.03); border-left: 1px solid rgba(148, 163, 184, 0.14); }
.ppanel__who { font-size: 15.5px; line-height: 1.55; color: var(--slate-200); margin-bottom: 18px; }
.ppanel__caps { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.ppanel__caps li { display: flex; gap: 11px; align-items: flex-start; font-size: 14.5px; line-height: 1.45; color: var(--slate-300); }
.ppanel__caps svg { flex: none; width: 20px; height: 20px; margin-top: 1px; padding: 3px; border-radius: 50%; stroke: var(--c); fill: none; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; background: color-mix(in srgb, var(--c) 14%, transparent); }
.ppanel.is-in .ppanel__caps li { animation: ppanelIn 0.45s var(--ease) both; }
.ppanel.is-in .ppanel__caps li:nth-child(1) { animation-delay: 0.10s; }
.ppanel.is-in .ppanel__caps li:nth-child(2) { animation-delay: 0.17s; }
.ppanel.is-in .ppanel__caps li:nth-child(3) { animation-delay: 0.24s; }
.ppanel.is-in .ppanel__caps li:nth-child(4) { animation-delay: 0.31s; }

@media (max-width: 760px) {
  .pplans { padding-top: 136px; }
  .pplans__row { grid-template-columns: repeat(2, 1fr); gap: 18px 8px; }
  .pbub { width: min(150px, 40vw); }
  .pbub::after { display: none; }   /* two rows of bubbles: a stem from the top row would run through the bottom one */
  .ppanel { grid-template-columns: 1fr; }
  .ppanel__in { border-left: 0; border-top: 1px solid rgba(148, 163, 184, 0.14); }
}
@media (prefers-reduced-motion: reduce) {
  .pbub, .pbub::after, .pplans::before { transition: none; }
  .ppanel.is-in, .ppanel.is-in .ppanel__caps li { animation: none; }
}

/* =========================================================
   THE PROJECT THREAD  (#journey on apps.html)
   ---------------------------------------------------------
   Owner steer, 2026-09-19: "revamp the whole project on one thread — show how the other products
   feed in, with a nice animation." Settled after nine mockups (mockups/journey/, never deployed).

   What it says: Complly Construct holds the record across all four CDM phases; Risk, FRA and
   Inspection each work in some of them. Point at a phase and that phase's detail opens beneath.

   🔑 Every phase card is a real <button role="tab">, not a div — it has to be reachable by Tab, and
   hovering is not an interaction a keyboard can perform.
   🔑 With assets/js/jthread.js blocked nothing is lost: the markup ships with Construction open, and
   the entrance state (.is-in) is what the script ADDS, so a script-less page shows everything.
   ========================================================= */
.jr { position: relative; padding: clamp(54px, 6vw, 86px) 0 clamp(48px, 5vw, 72px); }
.jr__head { text-align: center; max-width: 720px; margin: 0 auto clamp(26px, 3vw, 40px); }
.jr__head h2 { font-size: clamp(30px, 3.6vw, 46px); line-height: 1.08; color: var(--white); margin: 0; letter-spacing: -0.03em; }
.jr__head p { margin: 14px 0 0; font-size: clamp(15px, 1.2vw, 17px); line-height: 1.6; color: var(--slate-400); }
.jr__head b { color: var(--slate-200); font-weight: 600; }
.jr__foot { margin: clamp(22px, 2.6vw, 34px) auto 0; max-width: 780px; text-align: center; font-size: 13.5px; line-height: 1.6; color: var(--slate-500); }
.jr__foot b { color: var(--slate-200); font-weight: 600; }

/* One switch per product: --c is set by data-p and EVERYTHING below reads it, so a colour cannot
   half-apply the way it could if each part named its own. */
.jr [data-p="construct"] { --c: var(--p-construct); }
.jr [data-p="risk"] { --c: var(--p-risk); }
.jr [data-p="fra"] { --c: var(--p-fra); }
.jr [data-p="inspect"] { --c: var(--p-inspect); }
.jr__tick { width: 15px; height: 15px; flex: none; fill: none; stroke: var(--c, var(--green)); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }

/* ---- Complly Construct, named once and large (owner: "you repeat it twice — centre it, make it
        larger, with 'holds the record through all four' underneath") ---- */
.jr__hero { text-align: center; margin: clamp(22px, 2.6vw, 34px) 0 clamp(20px, 2.4vw, 30px); }
.jr__heroName {
  display: inline-flex; align-items: center; gap: 12px; margin: 0;
  font-family: var(--font-head); font-weight: 800; letter-spacing: -0.025em;
  font-size: clamp(24px, 2.8vw, 36px); line-height: 1.1; color: var(--white);
}
.jr__heroName i { width: 11px; height: 11px; border-radius: 50%; flex: none; background: var(--p-construct); box-shadow: 0 0 16px var(--p-construct); }
.jr__heroName span { color: var(--p-construct); }
.jr__heroSub { margin: 8px 0 0; font-size: clamp(14px, 1.15vw, 16px); line-height: 1.5; color: var(--slate-400); }

/* ---- the four phase cards ---- */
.jphases { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; }
.jphase {
  display: flex; flex-direction: column; gap: 0; width: 100%; text-align: left;
  font: inherit; cursor: pointer; color: inherit;
  padding: 16px 16px 18px; border-radius: 16px; opacity: 0; transform: translateY(16px);
  background: rgba(255, 255, 255, 0.035); border: 1px solid rgba(148, 163, 184, 0.18);
  transition: opacity .55s var(--ease), transform .55s var(--ease), background .3s var(--ease), border-color .3s var(--ease), box-shadow .3s var(--ease);
}
.jphase:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }
.is-in .jphase { opacity: 1; transform: none; }
.is-in .jphase:nth-child(1) { transition-delay: .35s, .35s, 0s, 0s, 0s; }
.is-in .jphase:nth-child(2) { transition-delay: .48s, .48s, 0s, 0s, 0s; }
.is-in .jphase:nth-child(3) { transition-delay: .61s, .61s, 0s, 0s, 0s; }
.is-in .jphase:nth-child(4) { transition-delay: .74s, .74s, 0s, 0s, 0s; }
.jphases.is-lit .jphase { opacity: .55; }
.jphase[aria-selected="true"] {
  background: rgba(255, 255, 255, 0.075); border-color: var(--c);
  box-shadow: 0 20px 46px -22px var(--c); transform: translateY(-3px);
}
/* Selected beats dimmed, so the open phase never fades when the pointer wanders off it. */
.jphases.is-lit .jphase[aria-selected="true"] { opacity: 1; }
.jphase__top { display: flex; align-items: center; gap: 9px; }
.jphase__top i { width: 9px; height: 9px; border-radius: 50%; flex: none; background: var(--c); box-shadow: 0 0 8px var(--c); }
.jphase__top b { font-family: var(--font-head); font-weight: 700; font-size: 14.5px; color: var(--white); }
.jphase__list { display: grid; gap: 9px; margin-top: 13px; flex-grow: 1; }
.jphase__item { display: flex; gap: 9px; align-items: flex-start; font-size: 13px; line-height: 1.4; color: var(--slate-300); }

/* The products that work in a phase are chips INSIDE that phase's card (owner: "have the box appear
   where it activates on the actual app itself"), not a separate row of boxes to line up with. */
.jchips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 14px; padding-top: 13px; border-top: 1px solid rgba(148, 163, 184, 0.16); }
.jchip {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: 999px;
  font-family: var(--font-head); font-weight: 700; font-size: 11px; color: var(--white);
  background: color-mix(in srgb, var(--c) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--c) 50%, transparent);
}
.jchip i { width: 6px; height: 6px; border-radius: 50%; background: var(--c); }
.jchips--none { display: block; font-size: 11.5px; color: var(--slate-500); font-style: italic; }

/* ---- the animated thread, BELOW the cards (owner: "the four boxes, then the line with the
        animation going through it") ---- */
.jspine { position: relative; height: 4px; border-radius: 3px; background: rgba(148, 163, 184, 0.18); margin: clamp(20px, 2.4vw, 30px) 0 0; }
.jspine__fill {
  position: absolute; inset: 0; border-radius: 3px; transform: scaleX(0); transform-origin: left center;
  background: linear-gradient(90deg, color-mix(in srgb, var(--p-construct) 55%, transparent), var(--p-construct));
  transition: transform 1.5s cubic-bezier(.22, 1, .36, 1);
}
.is-in .jspine__fill { transform: scaleX(1); }
.jspine__pulse {
  position: absolute; top: -3px; width: 70px; height: 9px; border-radius: 9px; opacity: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .85), transparent);
}
.is-in .jspine__pulse { animation: jrPulse 4.6s linear 1.5s infinite; }
@keyframes jrPulse { 0% { left: 0; opacity: 0; } 8%, 92% { opacity: 1; } 100% { left: 100%; opacity: 0; } }

/* ---- Construct's own rail: four matching boxes, one per phase ----
   It fills ALL FOUR columns while the other three appear in one or two. That difference IS the
   claim, so the rail is drawn as four ordinary boxes rather than welded into one bar — the welded
   version had rounded outer ends and square inner joins and read as a rendering fault. */
/* No label of its own: the rail is named ONCE, by the centred heading above it (owner: "you repeat
   Complly Construct twice"). Do not re-add one here. */
.jrail { margin-top: 16px; padding-bottom: 16px; }
/* The same 4-column track and the same 18px gap as .jphases above — that is what makes the rail line
   up with the cards. Change one and change the other. */
.jrail__track { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; }
.jcell {
  height: 34px; border-radius: 10px; display: flex; align-items: center; padding: 0 11px; overflow: hidden;
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.05), transparent);
  border: 1px solid var(--c); border-left-width: 3px;
  transition: opacity .3s var(--ease), border-color .3s var(--ease);
}
.jcell span { font-size: 11.5px; line-height: 1.25; color: var(--slate-300); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.jcell.is-sel { border-color: color-mix(in srgb, var(--c) 45%, transparent); }

/* ---- the detail panel ---- */
.jdock { margin-top: 26px; animation: jrDrawer .45s var(--ease) both; }
@keyframes jrDrawer { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: none; } }
.jpanel { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 20px; align-items: stretch; }
/* ONE HEIGHT FOR EVERY PANEL (owner: "they seem to shrink and expand — keep it all the same size").
   Measured before: 222 / 242 / 321 / 222, a 99px swing that moved the page under the pointer. All
   four share one grid cell, so the dock is always as tall as the tallest.
   The unselected ones keep [hidden] but are painted with visibility, not display: visibility:hidden
   keeps the BOX while still removing them from the accessibility tree and the tab order.
   ⚠️ Keep this the LAST [hidden] rule for .jpanel — a later, equally specific display:none silently
   undoes it, which is exactly what happened once on the pricing dock (2026-09-19). */
.jdock { display: grid; }
.jdock > .jpanel { grid-area: 1 / 1; }
.jpanel[hidden] { display: grid; visibility: hidden; pointer-events: none; }
.jpanel > .jout, .jpanel > .jfeed { height: 100%; box-sizing: border-box; }
.jlbl { margin: 0; font-family: var(--font-head); font-weight: 700; font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--c); }
.jout {
  display: flex; flex-direction: column; padding: 22px 24px; border-radius: 20px;
  background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--c) 13%, transparent) 0%, rgba(11, 23, 41, 0.6) 58%);
  border: 1px solid color-mix(in srgb, var(--c) 42%, transparent);
}
/* The four items STRETCH to fill the box (owner: "spread them out — there's a big bit of space
   underneath"). Measured 147–185px of dead space at the foot of the 405px box on every phase.
   They are always exactly four, which is what makes stretching safe here. */
.jout ul { list-style: none; margin: 16px 0 4px; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; flex-grow: 1; grid-auto-rows: 1fr; }
.jout li {
  display: flex; gap: 10px; align-items: center; padding: 12px 13px; border-radius: 12px;
  font-size: 13.5px; line-height: 1.4; color: var(--slate-200);
  background: rgba(255, 255, 255, 0.045); border: 1px solid rgba(148, 163, 184, 0.14);
  animation: jrFly .45s var(--ease) both;
}
.jout li:nth-child(2) { animation-delay: .06s; }
.jout li:nth-child(3) { animation-delay: .12s; }
.jout li:nth-child(4) { animation-delay: .18s; }
@keyframes jrFly { from { opacity: 0; transform: translateX(-14px) scale(.96); } to { opacity: 1; transform: none; } }
.jout__note { margin: 14px 0 0; font-size: 13px; line-height: 1.6; color: var(--slate-400); }
.jfeed { display: flex; flex-direction: column; padding: 22px 24px; border-radius: 20px; background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(148, 163, 184, 0.16); }
.jfeed__lbl { margin: 0; font-family: var(--font-head); font-weight: 700; font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--slate-500); }
/* This list holds one to four items, so stretching would balloon a single row. It spreads instead:
   the items keep their natural height and the SPACE around them grows. */
.jfeed ul { list-style: none; margin: 16px 0 0; padding: 0; display: grid; gap: 12px; flex-grow: 1; align-content: center; }
.jfeed li {
  display: flex; gap: 12px; align-items: flex-start; padding: 13px 14px; border-radius: 14px;
  background: color-mix(in srgb, var(--c) 8%, transparent); border: 1px solid color-mix(in srgb, var(--c) 35%, transparent);
  animation: jrFly .45s var(--ease) both;
}
.jfeed li:nth-child(2) { animation-delay: .09s; }
.jfeed li:nth-child(3) { animation-delay: .18s; }
.jfeed i { flex: none; width: 30px; height: 30px; border-radius: 50%; border: 2px solid var(--c); box-shadow: 0 0 0 5px color-mix(in srgb, var(--c) 10%, transparent); }
.jfeed b { display: block; font-family: var(--font-head); font-weight: 700; font-size: 14px; color: var(--white); }
.jfeed b span { color: var(--c); }
.jfeed small { display: block; margin-top: 3px; font-size: 12.5px; line-height: 1.4; color: var(--slate-400); }
.jfeed__solo { margin: auto 0 0; padding-top: 12px; font-size: 13px; line-height: 1.55; color: var(--slate-500); }

/* The two audience buttons + Ask Rodney, moved out of the left column into a centred block between
   the bubble stage and this section (owner, 2026-09-19). The "Our apps" pill and the "Not sure where
   to start?" label went with them.
   🔑 It sits BETWEEN the stage's .container and this section's, so it has no gutter of its own —
   measured at 375: the full-width buttons ran 0→375, hard against both screen edges. */
.astage__pick {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  margin: clamp(26px, 3vw, 40px) auto 0;
  padding-inline: 24px;
}
.astage__pick .awho { margin: 0; gap: 12px; justify-content: center; }
.astage__pick .hero__rodney { margin: 0; }

@media (max-width: 900px) {
  .jphases { grid-template-columns: 1fr 1fr; }
  .jpanel { grid-template-columns: 1fr; }
  .jout ul { grid-template-columns: 1fr; }
  .jrail__track { gap: 6px; }
  .jcell { padding: 0 6px; }
  .jcell span { display: none; }
}
@media (max-width: 560px) {
  .jphases { grid-template-columns: 1fr; }
  .jspine { display: none; }
  .astage__pick .awho { flex-direction: column; width: 100%; max-width: 420px; }
  .astage__pick .awho__btn { width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .jspine__fill, .jphase { transition: none !important; transform: none !important; opacity: 1 !important; }
  .jspine__pulse, .jout li, .jfeed li, .jdock { animation: none !important; }
}
