/*
 * Telyne — composite section patterns.
 * ---------------------------------------------------------------------------
 * Built from the primitives in components.css. Shared across pages so a pattern
 * exists once: the capability catalogue, the journey stage and the code panel
 * appear on more than one page and must not diverge.
 *
 * No hex in this file. Area and state colours arrive as --chip-fg / --chip-bg
 * set on the element from a token.
 */

/* ===========================================================================
 * Hero
 * ======================================================================== */

.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  /* VERTICAL, and that is the whole point.
   *
   * This was 170deg. A tilted gradient's iso-colour lines are tilted too, so
   * across 1280px of width the same colour lands at y positions 226px apart —
   * absorbed by a 654px hero, not by a 299px one. On the contact page the
   * bottom edge was still mid-blue and 35 levels darker on the left than the
   * right, against a white section below it.
   *
   * Tuning the stops for one height moves the problem to the next height. A
   * `to bottom` gradient's line is exactly the box height whatever the width,
   * so every stop resolves inside the box at ANY height. The 10-degree tilt is
   * what is given up, and it is what caused this.
   *
   * A translucent overlay was tried first and cannot work: .hero::before and
   * .hero::after sit at z-index -1, which is ABOVE this background. See the
   * aurora note below. */
  background: linear-gradient(
    to bottom,
    var(--surface) 0%,
    var(--tint-azure) 55%,
    var(--surface) 100%
  );
  border-bottom: 1px solid var(--border);
}

/*
 * The aurora (§3): two blurred fields, brand and cyan, at very low opacity,
 * drifting on a 26s cycle. Decorative and pointer-transparent, behind
 * everything, and clipped by the hero so it can never extend the document.
 */
.hero::before,
.hero::after {
  content: "";
  position: absolute;
  z-index: -1;
  pointer-events: none;
  border-radius: var(--radius-pill);
  filter: blur(70px);
  will-change: transform;
}

.hero::before {
  width: 46%;
  aspect-ratio: 1;
  top: -12%;
  left: -8%;
  background: var(--brand-400);
  opacity: 0.16;
  animation: aurora-a 18s var(--ease-in-out) infinite;
}

.hero::after {
  width: 38%;
  aspect-ratio: 1;
  bottom: -18%;
  right: -6%;
  background: var(--accent-cyan);
  opacity: 0.14;
  animation: aurora-b 18s var(--ease-in-out) infinite;
}

@keyframes aurora-a {
  0%,
  100% {
    transform: translate3d(0, 0, 0) scale(1);
  }
  50% {
    transform: translate3d(6%, 8%, 0) scale(1.12);
  }
}

@keyframes aurora-b {
  0%,
  100% {
    transform: translate3d(0, 0, 0) scale(1.08);
  }
  50% {
    transform: translate3d(-7%, -6%, 0) scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero::before,
  .hero::after {
    animation: none;
  }
}

@media (min-width: 1024px) {
  .hero {
    /* The bottom was space-24; with the band beneath it that summed to 121px. */
    padding-block: var(--space-20) var(--space-12);
  }
}

/* ===========================================================================
 * .hero--plain — the one-column hero
 *
 * .hero is a TWO-COLUMN band: .hero-grid puts the copy left and the
 * illustration right from 1024 up. The contact page has no illustration and was
 * given .hero--plain and .hero-copy to say so. Neither class existed, so the
 * copy sat flush left in a 1184px row with 570px of nothing beside it at 1280.
 *
 * DESIGN-AUTHORITY §4: "Section headers are centred — eyebrow, heading,
 * sub-line, in a 720px block. Exceptions: the hero and the facts band only."
 * The hero is exempt BECAUSE it has a second column to balance against. This
 * variant has none, so the exemption does not reach it and the rule does.
 * ======================================================================== */
/* No aurora. The class is called plain and this is what plain means.
 *
 * .hero::before and .hero::after are two blurred fields, 46% and 38% of the
 * hero's WIDTH with aspect-ratio 1, offset by a fraction of its height. In the
 * homepage's 1280x654 hero that is a 588px and a 486px circle, which the band
 * is tall enough to absorb — its bottom edge measures uniform across the full
 * width. The same rule in a 1280x360 band produces the same two circles inside
 * a box 294px shorter, so both cover the whole band including its bottom edge.
 * They sit at z-index -1, which is above the element's background, so they and
 * not the gradient were painting that edge blue on the left and cyan on the
 * right.
 *
 * Capping their size was tried and is the wrong direction twice over: it
 * changes the base component in order to fix a variant, and because ::after is
 * bottom-anchored, shortening it moves its centre TOWARDS the edge and
 * concentrates the glow there. Measured, the sample at x=1024 moved four
 * levels further from white rather than closer to it; the two colour values
 * are in the commit message for 8c67a7d, which is where a measurement can be
 * written down without putting a hex in a stylesheet.
 *
 * A 360px banner does not need a 500px aurora. Removing it here leaves the
 * homepage untouched. */
.hero--plain::before,
.hero--plain::after {
  content: none;
}

.hero-copy {
  max-width: 45rem;          /* the 720px block §4 specifies */
  margin-inline: auto;
  text-align: center;
}

/* .lede caps at 34em, which is narrower than the block and would sit left
 * inside it. Centred, it is the measure §4 asks for. */
.hero--plain .hero-copy .lede {
  margin-inline: auto;
}

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-12);
  align-items: center;
}

.hero-grid > * {
  min-width: 0;
}

@media (min-width: 1024px) {
  .hero-grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
    gap: var(--space-10);
  }
}

/*
 * The base step is display-s; REVISION-9 §1 near the end of this file carries
 * the measurements and raises it to display-l from 1280.
 *
 * This comment previously argued for display-m against the headline "All your
 * telecom.", which revision 9 replaced, and it sat directly above a rule
 * setting display-s. Both the copy and the conclusion were out of date.
 */
.hero h1 {
  margin-top: var(--space-6);
  font-size: var(--text-display-s);
  font-weight: 800;
  line-height: var(--leading-display);
  letter-spacing: var(--tracking-display);
}

.hero h1 .alt {
  color: var(--brand-600);
  display: block;
}

.lede {
  margin-top: var(--space-6);
  max-width: 34rem;
  font-size: var(--text-lg);
  line-height: 1.55;
  color: var(--text-secondary);
}

.ctas {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-8);
}

/*
 * THE SWITCH-ON ROW — REVISION-2 §3
 *
 * Replaces the four ticks. A drifting rail of capability pills, each with a
 * toggle glyph in the "on" position, so breadth and the switch-on model land in
 * one glance.
 *
 * The track is duplicated to loop seamlessly; the copy is aria-hidden so it is
 * announced once. Drift pauses on hover and on keyboard focus. Under reduced
 * motion the animation is off entirely and the rail becomes an ordinary
 * horizontal scroller, so the content is still reachable.
 */
.switchon {
  margin-top: var(--space-8);
  overflow: hidden;
  /* Fades at both ends so pills enter and leave rather than being cut. */
  mask-image: linear-gradient(
    90deg,
    transparent 0,
    var(--grey-900) var(--space-8),
    var(--grey-900) calc(100% - var(--space-8)),
    transparent 100%
  );
}

.switchon-track {
  display: flex;
  width: max-content;
  animation: rail-drift 48s linear infinite;
}

/* Two identical halves, so -50% lands exactly on the seam. The second is
 * aria-hidden, so the rail is announced once. */
.switchon-half {
  display: flex;
  gap: var(--space-2);
  padding-right: var(--space-2);
}

.switchon:hover .switchon-track,
.switchon:focus-within .switchon-track {
  animation-play-state: paused;
}

@keyframes rail-drift {
  from {
    transform: translate3d(0, 0, 0);
  }
  to {
    /* Exactly half, because the track holds two identical copies. */
    transform: translate3d(-50%, 0, 0);
  }
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--surface);
  box-shadow: var(--shadow-xs);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text-secondary);
  white-space: nowrap;
}

.pill--more {
  background: var(--tint-azure);
  color: var(--on-azure);
}

/* The toggle glyph, in the "on" position. */
.pill i {
  position: relative;
  width: 18px;
  height: 11px;
  flex: none;
  border-radius: var(--radius-pill);
  background: var(--on-green);
}

.pill i::after {
  content: "";
  position: absolute;
  top: var(--space-05);
  right: var(--space-05);
  width: 7px;
  height: 7px;
  border-radius: var(--radius-pill);
  background: var(--surface);
}

.no-js .switchon {
  overflow-x: auto;
}

.no-js .switchon-track {
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .switchon {
    overflow-x: auto;
  }

  .switchon-track {
    animation: none;
  }
}

/* ===========================================================================
 * THE HERO ILLUSTRATION — REVISION-2 §4
 *
 * Rebuilt from hero-reference.png: capability tiles rather than text chips,
 * each a rounded square holding a coloured icon with its label beneath; a
 * layered dimensional core; two dotted orbit rings the tiles sit on; thin
 * curved connectors; and a "+38 more" tile.
 *
 * The panel is borderless and the artwork fills it, per §7.1 and §4.
 * ======================================================================== */

.hv {
  position: relative;
  border-radius: var(--radius-xl);
  background: transparent;
  min-width: 0;
}

.hv-svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.hv-cap {
  margin-top: var(--space-2);
  text-align: center;
}

/* --- gradient stops and fills ------------------------------------------ */

.hv .sa0 { stop-color: var(--grey-0); }
.hv .sa1 { stop-color: var(--brand-50); }
.hv .sb0 { stop-color: var(--brand-100); }
.hv .sb1 { stop-color: var(--brand-200); }
.hv .sc0 { stop-color: var(--brand-200); }
.hv .sc1 { stop-color: var(--brand-300); }
.hv .gg0 { stop-color: var(--brand-200); }
.hv .gg1 { stop-color: var(--bg); }

.hv feDropShadow { flood-color: var(--grey-900); }

/* Reduced, so the tiles have something to sit against rather than dissolving
 * into it. */
.hv-glow {
  fill: url(#hvGlow);
  opacity: 0.28;
}

/* Two faint dotted rings; the tiles sit on them. */
.hv-ring {
  fill: none;
  stroke: var(--brand-300);
  stroke-width: 1.25;
  stroke-dasharray: 1.5 9;
  stroke-linecap: round;
  opacity: 0;
  animation: hv-ringin 400ms var(--ease-out) 80ms forwards;
}

.hv-ring--o {
  animation-delay: 180ms;
}

@keyframes hv-ringin {
  to { opacity: 0.85; }
}

/* --- connectors: drawn from core to tile ------------------------------- */



/* --- the layered core --------------------------------------------------- */

.hv-core {
  filter: url(#hvCore);
}

.hv-coreshadow {
  fill: var(--grey-900);
  opacity: 0.09;
}

/* Bottom slab first, each layer settling on top 120ms apart. */
.hv-slab {
  opacity: 0;
  animation: hv-settle 300ms var(--ease-out) forwards;
}

.hv-slab--3 {
  fill: var(--brand-400);
  animation-delay: 60ms;
}

.hv-slab--2 {
  fill: var(--brand-300);
  animation-delay: 180ms;
}

.hv-slab--1 {
  fill: var(--grey-0);
  stroke: var(--brand-100);
  stroke-width: 1;
  animation-delay: 300ms;
}

@keyframes hv-settle {
  from {
    opacity: 0;
    transform: translateY(-14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.hv-lockup {
  opacity: 0;
  animation: hv-fade 260ms var(--ease-out) 460ms forwards;
}

@keyframes hv-fade {
  to { opacity: 1; }
}

/* A soft glow pulse once the stack has assembled. */
.hv-core::after {
  content: "";
}

.hv-glow {
  animation: hv-glowpulse 1200ms var(--ease-out) 560ms;
}

@keyframes hv-glowpulse {
  0% { opacity: 0.5; }
  40% { opacity: 0.85; }
  100% { opacity: 0.5; }
}

/* --- capability tiles --------------------------------------------------- */

.ct {
  opacity: 0;
  animation: hv-tilein 300ms var(--ease-out) var(--ct-d, 0ms) forwards;
}

.ct-in {
  transition: transform var(--duration-base) var(--ease-out);
}

/* A slight overshoot on arrival, then settle. */
@keyframes hv-tilein {
  0% {
    opacity: 0;
    transform: scale(0.85);
  }
  70% {
    opacity: 1;
    transform: scale(1.04);
  }
  100% {
    opacity: 1;
    transform: none;
  }
}

.ct-box {
  fill: var(--grey-0);
  filter: url(#hvLiftStrong);
}

.ct-ico .knock-s {
  stroke: var(--grey-0);
  fill: none;
}

.ct-ico .knock {
  fill: var(--grey-0);
}

/* Full saturation. At 0.82 the icons read as washed against a pale wash. */
.ct-ico {
  fill: var(--ct-c, var(--brand-500));
  color: var(--ct-c, var(--brand-500));
  opacity: 1;
  transition: opacity var(--duration-fast) var(--ease-out);
}

.ct-t {
  fill: var(--text-secondary);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 600;
}

.ct-more {
  fill: var(--on-azure);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 700;
}

.ct--more .ct-box {
  fill: var(--tint-azure);
}

/* Hover: the tile lifts, its icon goes to full colour, its connector brightens.
 * Pointer-gated, so a tap never leaves a tile stuck lifted. */
@media (hover: hover) and (pointer: fine) {
  .ct:hover .ct-in {
    transform: translateY(-4px);
  }

  .ct:hover .ct-ico {
    opacity: 1;
  }
}

/* --- ambient rotation and parallax -------------------------------------- */

/*
 * The whole assembly drifts +/-1.5 degrees over 20s and the tiles counter-
 * rotate by the same amount so their labels stay upright. Both are transform
 * work on the compositor; no layout is touched.
 */
/* REVISION-3 §7: the ambient rotation is removed, not slowed. At 20s it was
 * still visibly moving the tile labels beside the headline while it was being
 * read, which is the thing the rule forbids. */
.hv-rot {
  transform-origin: 340px 296px;
}

/* No counter-rotation needed once nothing rotates. */

@keyframes hv-drift {
  0%, 100% { transform: rotate(-1.5deg); }
  50% { transform: rotate(1.5deg); }
}

@keyframes hv-counter {
  0%, 100% { rotate: 1.5deg; }
  50% { rotate: -1.5deg; }
}

/* Parallax against the cursor; the offset is written by site.js. */
.hv-par {
  transform: translate3d(var(--px, 0px), var(--py, 0px), 0);
  transition: transform 300ms var(--ease-out);
}

/* --- no scripting, and reduced motion ----------------------------------- */

.no-js .hv-ring,
.no-js .ct,
.no-js .hv-slab,
.no-js .hv-lockup {
  opacity: 1;
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .hv-ring,
  .ct,
  .hv-slab,
  .hv-lockup {
    opacity: 1;
    animation: none;
  }

  .hv-ring {
    opacity: 0.85;
  }

  .cl {
    stroke-dashoffset: 0;
    animation: none;
  }

  .hv-rot,
  .hv-par,
  .ct .ct-in,
  .hv-core {
    animation: none;
    transform: none;
    rotate: none;
    transition: none;
  }

  .ct:hover .ct-in {
    transform: none;
  }
}

/* ===========================================================================
 * Structural facts
 * ======================================================================== */

.factrow {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
}

.factrow > * {
  min-width: 0;
}

@media (min-width: 640px) {
  .factrow {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 1024px) {
  .factrow {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
}

.fact {
  text-align: center;
  min-width: 0;
}

.fact b {
  display: block;
  font-size: var(--text-stat-lg);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: var(--tracking-display);
  color: var(--text);
}

.fact span {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--text-xs);
  line-height: 1.45;
  color: var(--text-muted);
}

/* ===========================================================================
 * Fragmented / unified panes
 * ======================================================================== */

.panes {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
}

.panes > * {
  min-width: 0;
}

@media (min-width: 1024px) {
  .panes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.pane {
  border-radius: var(--radius-xl);
  background: var(--surface);
  padding: var(--space-8);
  min-width: 0;
}

.pane--good {
  background: var(--tint-azure);
}

.pane h3 {
  font-size: var(--text-h2);
  font-weight: 700;
}

.pane ul {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

.pane li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--text-secondary);
  min-width: 0;
}

.pane li svg {
  flex: none;
  margin-top: 3px;
}

.pane--old li svg {
  color: var(--text-muted);
}

.pane--good li svg {
  color: var(--brand-600);
}

.pane--good li {
  color: var(--text);
}

/* ===========================================================================
 * Capability catalogue
 * ======================================================================== */

.captabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-8);
}

.tabb {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text-secondary);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.tabb:hover {
  border-color: var(--border-strong);
  color: var(--text);
}

/* §8: the indicator slides between positions. The pill itself no longer
 * re-animates on every change; a shared underline element moves instead. */
.tabb[aria-selected="true"] {
  background: var(--text);
  border-color: var(--text);
  color: var(--text-inverse);
  animation: pill-in 300ms var(--ease-out);
}

@keyframes pill-in {
  from {
    transform: scale(0.92);
  }
  to {
    transform: scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .tabb[aria-selected="true"] {
    animation: none;
  }
}

.tabb i {
  width: 8px;
  height: 8px;
  flex: none;
  border-radius: var(--radius-pill);
  background: var(--chip-fg, var(--brand-600));
}

.tabb .n {
  font-variant-numeric: tabular-nums;
  opacity: 0.65;
}

.cap-intro {
  margin-top: var(--space-6);
  max-width: 46rem;
  font-size: var(--text-body);
  color: var(--text-secondary);
}

.caplegend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  margin-top: var(--space-5);
}

.caplegend span {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--text-secondary);
}

.caplegend i {
  width: 8px;
  height: 8px;
  flex: none;
  border-radius: var(--radius-pill);
  background: var(--chip-fg);
}

.capgrid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
  margin-top: var(--space-8);
}

.capgrid > * {
  min-width: 0;
}

@media (min-width: 640px) {
  .capgrid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 1024px) {
  .capgrid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* Tiles restagger on every tab change (§3). site.js sets --i per tile and
 * replays the animation, so the new area arrives rather than swapping. */
.cap {
  display: block;
  width: 100%;
  min-width: 0;
  text-align: left;
  animation: cap-in 340ms var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 50ms);
  border-radius: var(--radius-lg);
  background: var(--card-tint, var(--bg-alt));
  box-shadow: inset 0 2px 0 0 var(--chip-fg, var(--brand-400));
  padding: var(--space-5);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-base) var(--ease-out),
    transform var(--duration-base) var(--ease-out);
}

@keyframes cap-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cap {
    animation: none;
  }
}

@media (hover: hover) and (pointer: fine) {
  .cap:hover {
    transform: translateY(-3px);
    box-shadow: var(--shadow-md);
  }
}

.cap:hover {
  box-shadow: inset 0 2px 0 0 var(--chip-fg, var(--brand-400)), var(--shadow-md);
}

.cap-hd {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-width: 0;
}

.cap b {
  font-size: var(--text-h3);
  font-weight: 600;
  letter-spacing: var(--tracking-heading);
  min-width: 0;
  overflow-wrap: anywhere;
}

.cap p {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--text-secondary);
}

/*
 * The market note is always in the DOM — it is part of the honesty of the tile,
 * not a progressive enhancement. Collapsed it is clipped; expanded it is shown.
 * Height is animated rather than display, so it can transition.
 */
.mkt {
  display: block;
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  font-size: var(--text-xs);
  line-height: 1.5;
  color: var(--text-muted);
  transition: max-height var(--duration-base) var(--ease-out),
    opacity var(--duration-base) var(--ease-out),
    margin-top var(--duration-base) var(--ease-out);
}

.cap[aria-expanded="true"] {
  background: var(--surface);
}

.cap[aria-expanded="true"] .mkt {
  max-height: 8rem;
  opacity: 1;
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}

.no-js .mkt {
  max-height: none;
  opacity: 1;
  margin-top: var(--space-3);
}

.cap-more {
  margin-top: var(--space-8);
  text-align: center;
}

/* ===========================================================================
 * Journey — steps and stage
 * ======================================================================== */

.journey {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-10);
  margin-top: var(--space-10);
}

.journey > * {
  min-width: 0;
}

/* §7.3: 5/7 rather than a balanced pair. */
@media (min-width: 1024px) {
  .journey {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    align-items: stretch;
  }

  /* REVISION-6 §6 — the seven-step list is tall and the panel short, so it
   * floated at the top over a large void. Sticky, and centred in the
   * viewport, so it tracks whichever step is being read. */
  /* The column stretches so both sides of the split end together; the panel
   * sticks inside it. A sticky grid item sat at content height and the two
   * columns finished 387px apart. */
  .stage-col {
    position: relative;
    height: 100%;
  }

  /* The sticky declaration itself lives after the base .stage rule below,
   * because that rule sets position: relative unconditionally and would
   * otherwise win on source order. */
}

.steps {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.step {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  width: 100%;
  min-width: 0;
  text-align: left;
  padding: var(--space-3) var(--space-4);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: transparent;
  font-family: inherit;
  color: inherit;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}

.step:hover {
  background: var(--surface-hover);
}

.step[aria-selected="true"] {
  background: var(--surface);
  border-color: var(--border);
  box-shadow: var(--shadow-xs);
}

.step em {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  flex: none;
  border-radius: var(--radius-pill);
  background: var(--border);
  color: var(--text-secondary);
  font-size: var(--text-micro);
  font-style: normal;
  font-weight: 700;
}

.step[aria-selected="true"] em {
  background: var(--brand-600);
  color: var(--text-inverse);
}

.step-t {
  min-width: 0;
}

.step b {
  display: block;
  font-size: var(--text-sm);
  font-weight: 600;
}

.step p {
  margin-top: var(--space-1);
  font-size: var(--text-xs);
  line-height: 1.5;
  color: var(--text-secondary);
}

.stage {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  background: var(--bg-sunken);
  box-shadow: var(--shadow-sm);
  padding: var(--space-6);
  min-width: 0;
  /* The frames are stacked, so the stage takes the height of the tallest and
   * nothing below it moves when the selection changes. */
  display: grid;
}

/* REVISION-8 §4 — the panel sticks. This must sit after the base .stage
 * rule: that rule sets position: relative unconditionally, so an identical
 * selector earlier in the file lost on source order and the computed
 * position stayed relative. The panel then scrolled ~490px out of view
 * while the column heights still measured equal, which is why the old
 * equal-heights check saw nothing wrong. */
@media (min-width: 1024px) {
  .stage {
    position: sticky;
    top: calc(var(--nav-h) + var(--space-6));
  }
}


.frame {
  grid-area: 1 / 1;
  min-width: 0;
  opacity: 0;
  transform: translateY(6px);
  visibility: hidden;
  transition: opacity var(--duration-base) var(--ease-out),
    transform var(--duration-base) var(--ease-out);
}

.frame.on {
  opacity: 1;
  transform: none;
  visibility: visible;
}

.no-js .frame {
  visibility: visible;
  opacity: 1;
  transform: none;
}

.no-js .frame:not(.on) {
  display: none;
}

.frame h3 {
  font-size: var(--text-h3);
  font-weight: 600;
}

.frame > p {
  margin-top: var(--space-1);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.mock {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-5);
}

.mrow {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  font-size: var(--text-sm);
  min-width: 0;
}

.mrow i {
  width: 8px;
  height: 8px;
  flex: none;
  border-radius: var(--radius-pill);
  background: var(--chip-fg, var(--brand-600));
}

.mrow code {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-muted);
  overflow-wrap: anywhere;
  text-align: right;
}

/* ===========================================================================
 * Code panel
 * ======================================================================== */

.codepanel {
  border-radius: var(--radius-xl);
  background: var(--ink-900);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  min-width: 0;
}

.codetabs {
  display: flex;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-3) 0;
  overflow-x: auto;
  scrollbar-width: none;
}

.codetabs::-webkit-scrollbar {
  display: none;
}

.tabc {
  /* §12: a control is 44px. These measured 30 in both engines. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  background: transparent;
  color: var(--on-dark-45);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out);
}

.tabc:hover {
  color: var(--text-inverse);
}

.tabc[aria-selected="true"] {
  background: var(--ink-800);
  color: var(--text-inverse);
}

.tabc:focus-visible {
  outline-color: var(--brand-400);
}

/*
 * THE CODE PANEL — REVISION-2 §5
 *
 * Previously every pane was stacked in one grid cell, so the panel took the
 * height of the tallest and the curl example sat above ~180px of dead dark
 * space. Now the stack is sized to the ACTIVE pane and transitions its height
 * over 220ms; site.js writes the measured height. A height change on tab
 * switch is far better than permanent emptiness.
 *
 * Panes are absolutely positioned so only the active one contributes anything
 * visible, and they still cross-fade.
 */
.codestack {
  position: relative;
  height: var(--code-h, auto);
  transition: height 220ms cubic-bezier(0.22, 1, 0.36, 1);
}

.codestack > * {
  position: absolute;
  inset: 0;
  min-width: 0;
  overflow-y: auto;
}

.codebox {
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--duration-base) var(--ease-out),
    visibility 0s linear var(--duration-base);
}

.codebox.on {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--duration-base) var(--ease-out), visibility 0s;
}

/* Before script runs, and without it, the stack is ordinary flow. */
.no-js .codestack,
.codestack:not([style*="--code-h"]) {
  height: auto;
}

.no-js .codestack > *,
.codestack:not([style*="--code-h"]) > * {
  position: relative;
  inset: auto;
}

.no-js .codebox {
  opacity: 1;
  visibility: visible;
}

.no-js .codebox:not(.on),
.codestack:not([style*="--code-h"]) .codebox:not(.on) {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .codestack {
    transition: none;
  }
}

/*
 * The panel holds position while the four cards scroll past it (§5). Both
 * columns are grid items and stretch to the same row height, so neither ends
 * before the other — which is what the gate measures.
 */
@media (min-width: 1024px) {
  .devpanel-col {
    position: relative;
  }

  .devpanel-col .codepanel {
    position: sticky;
    top: calc(var(--nav-h) + var(--space-6));
  }
}

.codebox {
  margin: 0;
  padding: var(--space-5);
  background: var(--ink-800);
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: 1.75;
  color: var(--on-dark-70);
  tab-size: 2;
}

.codebox code {
  font-family: inherit;
  white-space: pre;
}

/* ink-500 measures 2.30:1 on the panel — unreadable. This is 5.01:1. */
.codebox .c {
  color: var(--code-comment);
}

.codebox .k {
  color: var(--brand-300);
}

.codebox .s {
  color: var(--on-teal);
}

.codebox .n {
  color: var(--on-amber);
}

.codenote {
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--on-dark-10);
  background: var(--ink-900);
  font-size: var(--text-xs);
  color: var(--on-dark-45);
}

/* ===========================================================================
 * Operational console
 * ======================================================================== */

.console {
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  background: var(--surface);
  box-shadow: var(--shadow-md);
  overflow: hidden;
  min-width: 0;
}

.console-hd {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  background: var(--bg-sunken);
  min-width: 0;
}

.console-hd .note {
  white-space: nowrap;
}

.console-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
}

@media (min-width: 640px) {
  .console-body {
    grid-template-columns: 11rem minmax(0, 1fr);
  }
}

.console-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  padding: var(--space-4);
  border-bottom: 1px solid var(--border);
  min-width: 0;
}

@media (min-width: 640px) {
  .console-nav {
    flex-direction: column;
    flex-wrap: nowrap;
    border-bottom: 0;
    border-right: 1px solid var(--border);
  }
}

.console-nav span {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.console-nav span:first-child {
  background: var(--tint-brand-08);
  color: var(--brand-700);
  font-weight: 600;
}

.console-main {
  padding: var(--space-4);
  min-width: 0;
}

.enlist {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}





/* --- figures row (§6) --------------------------------------------------- */

.figrow {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}

@media (min-width: 1024px) {
  .figrow {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.figure {
  min-width: 0;
  padding: var(--space-4);
  border-radius: var(--radius-md);
  background: var(--bg-alt);
}

.figure-l {
  display: block;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.figure-v {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-stat);
  font-weight: 800;
  letter-spacing: var(--tracking-display);
  line-height: 1.15;
}

/* Wraps rather than pushing the delta out of the row: at 320 the sparkline
 * plus a five-character delta does not fit a half-width figure, and a fixed
 * sparkline meant the percentage escaped its parent by up to 31px. */
.figure-f {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  margin-top: var(--space-2);
  min-width: 0;
}

.spark {
  width: 62px;
  height: 18px;
  flex: 0 1 auto;
  min-width: 32px;
  overflow: visible;
}

.spark path {
  fill: none;
  stroke: var(--brand-400);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.figure-d {
  font-size: var(--text-xs);
  font-style: normal;
  font-weight: 600;
}

.figure-d--up {
  color: var(--on-green);
}

.figure-d--down {
  color: var(--on-coral);
}

/* --- usage chart -------------------------------------------------------- */

.chartwrap {
  margin-top: var(--space-5);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  background: var(--bg-alt);
  min-width: 0;
}

.chart-hd {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
}

.chart-hd b {
  font-size: var(--text-sm);
  font-weight: 600;
}

.chart-key {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
}

.chart-key > span {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.chart-key i {
  width: 8px;
  height: 8px;
  flex: none;
  border-radius: var(--radius-xs);
  background: var(--chip-fg);
}

.chart {
  display: block;
  width: 100%;
  height: 150px;
  margin-top: var(--space-3);
}

.ch-grid {
  stroke: var(--border);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.ch-y,
.ch-x {
  fill: var(--text-muted);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
}

/* Three clearly distinct hues. Blue, teal and green stacked into a muddy
 * gradient where the bands could not be told apart. */
.cg0--brand { stop-color: var(--brand-500); stop-opacity: .55; }
.cg1--brand { stop-color: var(--brand-500); stop-opacity: .04; }
.cg0--cyan  { stop-color: var(--accent-cyan); stop-opacity: .5; }
.cg1--cyan  { stop-color: var(--accent-cyan); stop-opacity: .04; }
.cg0--coral { stop-color: var(--accent-coral); stop-opacity: .45; }
.cg1--coral { stop-color: var(--accent-coral); stop-opacity: .04; }

.ch-fill--brand { fill: url(#chg-brand); }
.ch-fill--cyan  { fill: url(#chg-cyan); }
.ch-fill--coral { fill: url(#chg-coral); }

.ch-line {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ch-line--brand { stroke: var(--brand-500); }
.ch-line--cyan  { stroke: var(--accent-cyan); }
.ch-line--coral { stroke: var(--accent-coral); }

/* The bands draw in on entry, lowest first. */
.ch-fill,
.ch-line {
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out);
}

.chartwrap.in .ch-fill,
.chartwrap.in .ch-line {
  opacity: 1;
}

.chartwrap.in .ch-fill--cyan,
.chartwrap.in .ch-line--cyan { transition-delay: 50ms; }

.chartwrap.in .ch-fill--coral,
.chartwrap.in .ch-line--coral { transition-delay: 100ms; }

.no-js .ch-fill,
.no-js .ch-line { opacity: 1; }

/* Hover: guideline, a point on each band, and a floating panel. */
.ch-hover { opacity: 0; transition: opacity var(--duration-fast) var(--ease-out); }
.chartwrap.hot .ch-hover { opacity: 1; }

.ch-guide {
  stroke: var(--border-strong);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.ch-dot--brand { fill: var(--brand-500); }
.ch-dot--cyan  { fill: var(--accent-cyan); }
.ch-dot--coral { fill: var(--accent-coral); }

.ch-tip {
  position: absolute;
  z-index: 2;
  min-width: 9rem;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: var(--shadow-lg);
  font-size: var(--text-xs);
  pointer-events: none;
}

.ch-tip b {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--text-sm);
}

.ch-tip span {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--text-secondary);
}

.ch-tip i {
  width: 8px; height: 8px; flex: none;
  border-radius: var(--radius-xs);
}

.ch-tip em { margin-left: auto; font-style: normal; font-weight: 600; color: var(--text); }

.chartwrap { position: relative; }

@media (prefers-reduced-motion: reduce) {
  .ch-fill, .ch-line, .ch-hover { transition: none; opacity: 1; }
}

/* --- capability table and side column ----------------------------------- */

.console-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
  margin-top: var(--space-5);
}

.console-split > * {
  min-width: 0;
}

@media (min-width: 1280px) {
  .console-split {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  }
}

.enrow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--bg-alt);
  font-size: var(--text-sm);
  min-width: 0;
}

@media (min-width: 640px) {
  .enrow {
    grid-template-columns: minmax(0, 1.4fr) auto minmax(0, 1.2fr) auto;
  }
}

.enrow-n {
  font-weight: 600;
  min-width: 0;
  overflow-wrap: anywhere;
}

.enrow-u {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  min-width: 0;
}

.enrow-c {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-style: normal;
  color: var(--text);
  text-align: right;
  white-space: nowrap;
}

.console-side {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-width: 0;
}

.actlist,
.alertlist {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  background: var(--bg-alt);
  min-width: 0;
}

.actlist > .label,
.alertlist > .label {
  color: var(--text-muted);
  margin-bottom: var(--space-1);
}

.act {
  display: flex;
  gap: var(--space-3);
  min-width: 0;
}

.act time {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.act span {
  min-width: 0;
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.act b {
  display: block;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text);
}

.alert {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--surface);
  min-width: 0;
}

.alert i {
  width: 8px;
  height: 8px;
  flex: none;
  margin-top: 6px;
  border-radius: var(--radius-pill);
}

.alert--warn i {
  background: var(--on-amber);
}

.alert--ok i {
  background: var(--on-green);
}

.alert span {
  min-width: 0;
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.alert b {
  display: block;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text);
}

.console-foot {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

/* ===========================================================================
 * Segments, pricing model, trust
 * ======================================================================== */

.segments,
.trustgrid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
}

.segments > *,
.trustgrid > * {
  min-width: 0;
}

@media (min-width: 768px) {
  .segments,
  .trustgrid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 1024px) {
  .segments,
  .trustgrid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.seg,
.tr {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-xs);
  padding: var(--space-5);
  min-width: 0;
}

.seg b,
.tr b {
  display: block;
  margin-top: var(--space-4);
  font-size: var(--text-h3);
  font-weight: 600;
  letter-spacing: var(--tracking-heading);
}

.seg p,
.tr p {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--text-secondary);
}

.pmodel {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
}

.pmodel > * {
  min-width: 0;
}

@media (min-width: 1024px) {
  .pmodel {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.pm {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-xs);
  padding: var(--space-6);
  min-width: 0;
}

.pm b {
  display: block;
  font-size: var(--text-h2);
  font-weight: 700;
  letter-spacing: var(--tracking-heading);
}

.pm p {
  margin-top: var(--space-3);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-secondary);
}

.pnote {
  margin-top: var(--space-8);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: var(--space-6);
}

.pnote b {
  display: block;
  font-size: var(--text-h3);
  font-weight: 600;
}

.pnote p {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-secondary);
}

/* A single statement band. */
.callout {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  border: 1px solid var(--ring-brand-25);
  border-radius: var(--radius-xl);
  background: var(--tint-azure);
  padding: var(--space-6);
  min-width: 0;
}

.callout div {
  min-width: 0;
}

.callout b {
  display: block;
  font-size: var(--text-h2);
  font-weight: 700;
  letter-spacing: var(--tracking-heading);
}

.callout p {
  margin-top: var(--space-2);
  max-width: 42rem;
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--text-secondary);
}

/* ===========================================================================
 * Launch-list capture
 * ======================================================================== */

.capture {
  max-width: 32rem;
}

.capture--center {
  margin-inline: auto;
  text-align: center;
}

/* §7.3: the closing capture sits left with air to its right, like every
 * other heading block on the page. */
.capture {
  max-width: 34rem;
}

.capture form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-6);
  text-align: left;
}

@media (min-width: 480px) {
  .capture form {
    flex-direction: row;
    align-items: flex-start;
  }

  .capture .field {
    flex: 1 1 auto;
  }

  .capture .btn {
    flex: none;
  }
}

/* ===========================================================================
 * .capture--wide — a form with fields, not one field
 *
 * Everything above was written for the waiting-list capture: one email input
 * and a button, correctly one row from 480px up. The contact form reuses
 * .capture and has four controls and a message, so that same rule laid all five
 * children in a single row — three inputs at 87px, the textarea at 135px and
 * the button stranded at flex-start, identically in chromium and webkit at 768
 * and 1280. Below 480px it stacked and worked, so the form was completable on a
 * phone and nowhere else.
 *
 * The markup has carried .capture--wide since the page was written. This is the
 * rule it was asking for.
 *
 * TWO CLASSES ON PURPOSE. `.capture form` is one class and one type; so is
 * `.capture--wide form`, and equal specificity is decided by source order,
 * which is a fragile thing to depend on across a 3,000-line stylesheet.
 * `.capture.capture--wide form` wins outright, wherever it sits.
 * ======================================================================== */
.capture--wide {
  max-width: 45rem;
  margin-inline: auto;
}

/* The block is centred, so its own header is too — §4 again. The FIELDS stay
 * left-aligned: a centred label above a left-aligned input is a misalignment
 * the eye has to correct for on every row. */
.capture--wide > .sec-title,
.capture--wide > .sec-sub {
  text-align: center;
}

/* One control per row at EVERY width, including the widths where .capture
 * itself goes to a row. */
.capture.capture--wide form {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-4);
}

.capture.capture--wide .field {
  flex: none;
  width: 100%;
}

.capture.capture--wide .btn {
  flex: none;
  align-self: flex-start;
}

.capture .form-msg {
  margin-top: var(--space-3);
}

.capture--center .form-msg,
.capture--center .note {
  text-align: center;
}

/* ===========================================================================
 * REVISION-4 §2.2–§2.4 — hero
 * ======================================================================== */

/*
 * §2.4, the important one: the illustration has no container.
 *
 * It sat in a bordered white panel with padding, and that panel's padding box
 * was what clipped the outer tiles — the artwork was being asked to fit a box
 * narrower than itself. Removing the box removes the clipping without
 * shrinking anything, and lets the assembly use the whole column.
 */
.hv--bare {
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  padding: 0;
  overflow: visible;
}

/* §2.2 — 5/7, text narrower, illustration wider, vertically centred. */
@media (min-width: 1024px) {
  .hero-grid {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: var(--space-12);
    align-items: center;
  }
}

/* §2.2 — eyebrow pill with a leading dot. */
.eyebrow i {
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: var(--radius-pill);
  background: var(--brand-500);
}

/* §2.2 — two lines, very tight leading. */
.hero h1 {
  line-height: 1.02;
}

.hero h1 .alt {
  background-image: var(--gradient-signal);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* §2.2 — sub-line constrained to about 34em. */
.lede {
  max-width: 34em;
}

/* §2.3 — four items on one horizontal line, circular outline icons. */
.proofrow {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-6);
  margin-top: var(--space-8);
}

.proofrow span {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-muted);
  min-width: 0;
  white-space: nowrap;
}

.proofrow svg {
  flex: none;
}

/*
 * Below 1024 it becomes an even 2x2 rather than wrapping to an orphaned
 * fourth item, which is what §2.3 asks for and what the gate checks.
 */
@media (max-width: 1023px) {
  .proofrow {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3) var(--space-4);
  }

  .proofrow span {
    white-space: normal;
  }
}

/* §2.5 — below 640 the outer ring collapses: seven tiles plus "+38 more". */
@media (max-width: 639px) {
  .hv .ct[data-ring="outer"]:nth-of-type(n + 5) {
    display: none;
  }
}

/* ===========================================================================
 * REVISION-4 §3.9 — interactive elements that had no hover state
 *
 * Found by hovering each one and comparing computed style, after a first
 * attempt that read the stylesheet and reported every element as stateless
 * because cssRules throws on a file:// sheet. Measuring the wrong thing gives
 * a longer list, not a shorter one.
 * ======================================================================== */

.enrow {
  transition: background-color var(--duration-fast) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .enrow:hover {
    background: var(--surface-hover);
  }

  .act:hover b {
    color: var(--link);
  }
}

.act b {
  transition: color var(--duration-fast) var(--ease-out);
}

/* The secondary button's hover was surface -> surface-hover, a 2% step that
 * measured as no perceptible change. It now moves its border as well. */
.btn--secondary:hover {
  border-color: var(--text-muted);
}

/* ===========================================================================
 * REVISION-5 §3 — the flat capability grid
 *
 * Layout from the reference: ten cards, all visible, five across at 1280+.
 * Surface from Telyne: borderless tinted, per revision 2 §7.1. The reference's
 * white hairline card is precisely the treatment being avoided.
 * ======================================================================== */

.sec-head-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-4);
}

@media (min-width: 1024px) {
  .sec-head-row .sec-head {
    width: auto;
    max-width: 42rem;
  }
}

.sec-head-link {
  flex: none;
  padding-bottom: var(--space-1);
}

.capgrid-flat {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
  margin-top: var(--space-10);
  /* §4 — equal heights per row; the icon sits at the top and the description
   * fills the remainder, so varying copy length cannot break the grid. */
  align-items: stretch;
}

.capgrid-flat > * {
  min-width: 0;
}

@media (min-width: 480px) {
  .capgrid-flat {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* 1024-1279: two columns, not three. Ten cards over three columns is
 * 3+3+3+1, and an orphan row of one is what §7 forbids. Five even rows of
 * two satisfies both rules; the deviation from §3 is deliberate. */
@media (min-width: 1024px) {
  .capgrid-flat {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Five across at 1280 and up — two clean rows of five, no orphan row. */
@media (min-width: 1280px) {
  .capgrid-flat {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
}

.capcard {
  display: flex;
  flex-direction: column;
  border-radius: var(--radius-lg);
  background: var(--card-tint, var(--bg-alt));
  padding: var(--space-5);
  min-width: 0;
  transition: transform var(--duration-base) var(--ease-out),
    box-shadow var(--duration-base) var(--ease-out);
}

/* 36px rounded-square icon tile in the area colour on its tint. */
.capcard-ico {
  display: inline-flex;
  transform-origin: center;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--chip-fg, var(--brand-600));
  fill: currentColor;
  transition: filter var(--duration-base) var(--ease-out),
    transform var(--duration-base) var(--ease-out);
}

.capcard-ico .knock {
  fill: var(--surface);
}

.capcard-ico .knock-s {
  stroke: var(--surface);
  fill: none;
}

.capcard-t {
  margin-top: var(--space-4);
  font-size: var(--text-h3);
  font-weight: 600;
  letter-spacing: var(--tracking-heading);
}

.capcard-d {
  flex: 1;
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--text-secondary);
}

/* §6 — hover lifts 3px with a shadow step and the icon tile saturates. */
@media (hover: hover) and (pointer: fine) {
  .capcard:hover {
    transform: translateY(-3px);
    box-shadow: var(--shadow-md);
  }

  .capcard:hover .capcard-ico {
    filter: saturate(1.35);
    transform: scale(1.06);
  }
}

@media (prefers-reduced-motion: reduce) {
  .capcard {
    transition: none;
  }

  .capcard:hover {
    transform: none;
  }
}

.capgrid-note {
  margin-top: var(--space-6);
}

/* ===========================================================================
 * REVISION-5 §4 — developer section, three columns
 *
 * 3 / 5 / 4 of twelve. The two-column version left the right side empty; the
 * reference splits it three ways and fills it.
 * ======================================================================== */

.devcols {
  margin-top: var(--space-10);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-8);
}

.devcols > * {
  min-width: 0;
}

@media (min-width: 1024px) {
  .devcols {
    grid-template-columns: minmax(0, 4fr) minmax(0, 5fr) minmax(0, 3fr);
    gap: var(--space-10);
    /* Stretch, not start: with start each column ended at its own content
     * height and they finished up to 340px apart. */
    align-items: stretch;
  }
}

/* The list spreads to fill its column so the three end together. */
@media (min-width: 1024px) {
  .devcol--list .devlist {
    /* ~44px apart means pitch, not gap: a 20px item plus a 24px gap gives a
     * 44px pitch. A 40px gap gave 61px. */
    gap: var(--space-6);
  }

  .devcol--text {
    display: flex;
    flex-direction: column;
  }

  .devcol--text .link-arrow {
    margin-top: auto;
  }
}

.devcol--text p {
  margin-top: var(--space-4);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--on-dark-70);
}

.devcol--text .link-arrow {
  margin-top: var(--space-6);
}

/* A plain list — no cards, no borders, as the reference has it. */
.devlist {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.devlist li {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-inverse);
}

.devlist svg {
  flex: none;
  color: var(--brand-300);
}

/* Below 1024: text, then code, then the list as two columns. */
@media (max-width: 1023px) {
  .devlist {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-4);
  }
}

@media (max-width: 479px) {
  .devlist {
    grid-template-columns: minmax(0, 1fr);
  }
}


/* ===========================================================================
 * REVISION-3 §5 — the all-49 catalogue
 *
 * Five columns, one per area, every capability a compact row. Rendered into
 * the markup rather than by script, so the 49 names are in the HTML a crawler
 * reads rather than appearing only after a tab is clicked.
 * ======================================================================== */

.catgrid > * {
  transition-delay: calc(var(--i, 0) * 50ms);
}

/* ===========================================================================
 * No orphan in a capability headline
 *
 * Measured in both engines at 390 and 1440: chromium left a single word alone
 * on the last line of five capability h1s — "glance", "tickets", "bill",
 * "code", "field". WebKit broke the same text a word earlier and did not.
 * DESIGN-AUTHORITY's hero rule names an orphan as the one unacceptable outcome.
 *
 * Fixed here rather than by moving the <br> in h1_lines: the wrap point depends
 * on the viewport, and a break that fixes 1440 creates one at 390.
 * ======================================================================== */
.cp-capability .cp-hero h1 {
  text-wrap: pretty;
}

/* text-wrap: pretty cleared eight of the ten orphans. Two survived at 390 —
 * "glance" on API keys and "tickets" on SIM lifecycle — because at that width
 * the headline is pinned to the 40px floor of clamp(40px, 5.4vw, 60px) and the
 * second line simply cannot hold its last two words.
 *
 * DESIGN-AUTHORITY's hero rule names the remedy: "the headline takes the
 * largest type step at which it renders cleanly at each width. Stepping down at
 * a narrow width is acceptable. What is not acceptable is an orphan." So the
 * floor drops on phones only. 5.4vw passes 40px at 740, so this changes nothing
 * above that. */
@media (max-width: 520px) {
  .cp-capability .cp-hero h1 {
    font-size: 34px;
  }
}

/* ===========================================================================
 * .cp-tone — the band rhythm for pages built on the capability shell
 *
 * Thirty of thirty-one pages were white from the nav to the footer. The only
 * tonal variation on them was a motif at 5 to 14 per cent opacity, which is not
 * enough to make a page read as composed rather than as a scroll.
 *
 * THE RULE, and it is a rule rather than a preference:
 *   1. A toned band never touches another toned band.
 *   2. Tone marks REFERENCE sections — a spec table, a related grid, a code
 *      panel. Narrative sections stay on white, so a tone reads as a change of
 *      register rather than as a stripe.
 *   3. The closing capture is already .band--wash and counts as toned for
 *      rule 1, so the section above it is never toned.
 *
 * The treatment is not new: .band--wash fades --bg to --bg-alt and back with no
 * hairline, and the homepage has used it since it was built. This is that
 * treatment reaching the pages that never had one.
 *
 * HERE rather than in capability.css, which tools/extract-capability-css.js
 * regenerates. Scoped to .cp-capability so it reaches exactly the sections it
 * is meant to.
 * ======================================================================== */
.cp-capability .cp-tone {
  background: linear-gradient(
    180deg,
    var(--bg) 0%,
    var(--bg-alt) 12%,
    var(--bg-alt) 88%,
    var(--bg) 100%
  );
}

/* ===========================================================================
 * .cp-wrap--narrow — the reading measure the authored pages ask for
 *
 * 25 pages wrap their FAQ block in <div class="cp-wrap cp-wrap--narrow">: an
 * <h2> and a column of <details> accordions, all of it prose. The modifier
 * styled nothing, so it rendered at .cp-wrap's full 1160px — measured at
 * 1160px on about.html, identical to every other wrap on the page, including
 * the ones holding spec tables.
 *
 * 46rem is a reading measure and close to the 720px block DESIGN-AUTHORITY §4
 * gives a centred section header, so the accordions and the header above them
 * share an edge rather than missing each other by 200px.
 *
 * HERE RATHER THAN IN capability.css, which is generated by
 * tools/extract-capability-css.js from the approved reference: a rule written
 * there is discarded the next time that script runs. Scoped to .cp-capability
 * so it has the same reach as the rule it narrows.
 * ======================================================================== */
.cp-capability .cp-wrap.cp-wrap--narrow {
  max-width: 46rem;
}

/* THE TRACK COUNT WAS TYPED, AND IT WAS THE AREA COUNT.
 *
 * This was repeat(5, ...) at 1280 and it was not wrong when it was written: on
 * 3d58a54, the last build with five areas, five 218px tracks filled the 1184px
 * grid with 0px to spare. It looked full because it was full — a typed five
 * matching a data five. It is the same fault as every other typed count on this
 * site, and it surfaced the moment the data stopped agreeing: with two areas the
 * two columns sit in tracks 1 and 2 and 724px of the grid is empty.
 *
 * auto-fit computes the track count from the space available and collapses the
 * tracks nothing occupies, so this is correct at two areas, at five when the
 * empty areas return, and at any number in between. The three breakpoints go
 * with it — they were three more typed counts.
 *
 * 13.5rem is the floor, chosen so five tracks plus four gaps still fit the
 * 1184px grid: 5 x 216 + 4 x 24 = 1176. A wider floor would drop the fifth
 * column onto a second row the day the areas come back. */
.catgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr));
  gap: var(--space-8) var(--space-6);
  justify-items: center;
}

.catgrid > * {
  min-width: 0;
}

/* The column itself, which nothing defined.
 *
 * .catcol-h, .catcol-n and .catlist were all styled; the box holding them was
 * not, so it had no width of its own and no say in how the row distributed. It
 * takes its track, capped at a readable measure — with five areas the track is
 * 218px and the cap never binds, so that layout is unchanged; with two the
 * tracks stretch and the cap keeps a seven-item list from becoming a 580px
 * column of short links, while justify-items centres each column in its track
 * so the air is shared rather than piled up on the right. */
.catcol {
  width: 100%;
  max-width: 15rem;
}

.catcol-h {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding-bottom: var(--space-3);
  border-bottom: 2px solid var(--chip-fg, var(--brand-500));
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: var(--tracking-heading);
}

.catcol-n {
  margin-left: auto;
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--text-muted);
}

.catlist {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.catlist li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

/* §12: these measured 20-21px. The anchor is a flex item of the row, so a
 * min-height on it grows the row rather than overflowing it. */
.catlist a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}

.catlist i {
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: var(--radius-pill);
  background: var(--chip-fg);
}

.catlegend {
  margin-top: var(--space-10);
}

.catnote {
  margin-top: var(--space-4);
}

/* REVISION-9 §1 — the headline steps, as measured on the shipped page.
 *
 *   1024   display-s   32.67px   four lines
 *   1280   display-l   59.68px   four lines
 *   1440   display-l   60px      four lines
 *
 * The base rule above sets display-s; the §3 block at the end of this file
 * raises it to display-l from 1280.
 *
 * Four lines, not the two REVISION-9 §1 describes. With the approved copy —
 * "Every telecom service, one platform." / "Switch them on and off as you
 * like." — the longest segment needs 1014px at display-l against a 710px
 * column at 1280. No shippable step holds two lines at 1024 at all; only
 * display-xs at 15px does, which is body size. The two-line instruction and
 * the approved copy cannot both hold, and that is recorded in BACKLOG.md as a
 * decision rather than resolved here.
 *
 * The gate reports the step, the line count and the shortest line, and fails
 * only on an orphan — a single word alone on its own line. It does NOT assert
 * a line count; the earlier version of this comment claimed it did, which was
 * false, and claimed two lines, which was wrong by two. */

/* ===========================================================================
 * REVISION-7 §1 — the closing call to action is centred
 *
 * It was the one section that should be centred and was not: a 640px column
 * of content with an entirely empty right half, and the last thing a visitor
 * sees. Heading, sub-line, form and fine print all centre together.
 * ======================================================================== */

#join .sec-head,
#join .capture {
  width: 100%;
  max-width: 640px;
  margin-inline: auto;
  text-align: center;
}

#join .sec-sub {
  max-width: none;
  margin-inline: auto;
}

#join .capture form {
  justify-content: center;
}

#join .form-msg,
#join .note {
  text-align: center;
}

@media (min-width: 480px) {
  #join .capture form {
    flex-direction: row;
    align-items: flex-start;
  }
}

/* ===========================================================================
 * REVISION-7 §3 — continuous motion, four places only
 *
 * Every animation was an entrance: 126 elements all firing once and stopping,
 * so the page was static after the first scroll. These four keep living.
 * ======================================================================== */

/*
 * 3.1a The data pulse — the important one.
 *
 * A short bright segment travels the length of a connector FROM THE TILE INTO
 * THE CORE, one at a time, cycling through all eleven. The connectors are
 * drawn tile-to-core, so a dash animating from full offset to zero moves
 * inward. It shows capabilities feeding into one platform, which is the
 * product idea.
 */
/* 3.1b The core breathes on a 4s cycle — peripheral, not watchable. */
.hv-glow {
  animation: hv-breathe 4s var(--ease-in-out) infinite;
}

@keyframes hv-breathe {
  0%, 100% { opacity: 0.24; }
  50% { opacity: 0.36; }
}

/* 3.3 The chart's topmost band carries a slow shimmer every 8s, so the
 * console reads as live rather than as a screenshot. */
.ch-fill--coral {
  animation: ch-shimmer 8s var(--ease-in-out) infinite;
}

@keyframes ch-shimmer {
  0%, 88%, 100% { opacity: 1; }
  94% { opacity: 0.82; }
}

/* 3.4 A blinking cursor at the end of the active code example. */
.codebox.on code::after {
  content: "";
  display: inline-block;
  width: 7px;
  height: 1em;
  margin-left: 2px;
  vertical-align: text-bottom;
  background: var(--brand-300);
  animation: caret 1.1s steps(1) infinite;
}

@keyframes caret {
  0%, 50% { opacity: 1; }
  51%, 100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .hv-glow,
  .hv-arc--i,
  .hv-arc--o,
  .ct .ct-ico,
  .ch-fill--coral,
  .codebox.on code::after {
    animation: none;
  }

  .cl.pulse {
    animation: none;
    stroke-dasharray: none;
  }
}

/* ===========================================================================
 * REVISION-8 §3 — signal travels around the rings
 *
 * With the spokes gone the motion becomes one continuous system rather than a
 * sequence of entrances. Two bright arcs orbit the rings in opposite
 * directions; each is a dashed stroke on the ring circle, so it follows the
 * geometry exactly and can never drift out of alignment with the tiles.
 *
 * Circumferences: inner 2*pi*190 = 1194, outer 2*pi*286 = 1797. A 25 degree
 * arc is 1194*25/360 = 83 and 1797*25/360 = 125.
 * ======================================================================== */

.hv-arc {
  fill: none;
  stroke-linecap: round;
  opacity: 0.9;
}

.hv-arc--i {
  stroke: var(--brand-500);
  stroke-width: 3;
  stroke-dasharray: 83 1111;
  animation: arc-i 6s linear infinite;
}

.hv-arc--o {
  stroke: var(--accent-cyan);
  stroke-width: 3;
  stroke-dasharray: 125 1672;
  animation: arc-o 9s linear infinite;
}

/* Rotating the element is what makes the arc travel; the dash stays put on the
 * path, so the segment keeps its exact length the whole way round. */
@keyframes arc-i {
  to { transform: rotate(360deg); }
}

@keyframes arc-o {
  to { transform: rotate(-360deg); }
}

/*
 * §3 — each tile responds as an arc passes its angle. The delay is the tile's
 * own angle as a fraction of the revolution, so the response is locked to the
 * geometry and stays in sync forever rather than being scheduled separately.
 */
.ct-in {
  transition: transform var(--duration-base) var(--ease-out);
}

.ct[data-ring="inner"] .ct-ico,
.ct[data-ring="outer"] .ct-ico {
  animation: tile-respond 6s var(--ease-in-out) infinite;
  animation-delay: calc(var(--deg, 0) / 360 * -6s);
}

.ct[data-ring="outer"] .ct-ico {
  animation-duration: 9s;
  animation-delay: calc(var(--deg, 0) / 360 * 9s);
}

@keyframes tile-respond {
  0%, 92%, 100% { opacity: 0.86; }
  4% { opacity: 1; }
}

.ct[data-ring="inner"] .ct-box,
.ct[data-ring="outer"] .ct-box {
  animation: tile-lift 6s var(--ease-in-out) infinite;
  animation-delay: calc(var(--deg, 0) / 360 * -6s);
}

.ct[data-ring="outer"] .ct-box {
  animation-duration: 9s;
  animation-delay: calc(var(--deg, 0) / 360 * 9s);
}

@keyframes tile-lift {
  0%, 92%, 100% { transform: translateY(0); }
  4% { transform: translateY(-2px); }
}

.no-js .hv-arc,
.no-js .ct-ico,
.no-js .ct-box {
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .hv-arc {
    display: none;
  }

  .ct-ico,
  .ct-box,
  .hv-glow {
    animation: none;
  }
}

/* §1 — the right-hand link now sits beneath the sub-line, centred. */
.sec-head-more {
  margin-top: var(--space-5);
}

/* §5.1 — the code block was cutting a comment mid-word behind a scrollbar.
 * Comments wrap; code lines still scroll, which is correct for code. */
.codebox .c {
  color: var(--code-comment);
  white-space: pre-wrap;
}

/* §5.2 / §5.3 — the three columns share a top edge and finish together. */
@media (min-width: 1024px) {
  .devcols > * {
    align-self: stretch;
  }

  .devcol--list .devlist {
    height: 100%;
    justify-content: space-evenly;
  }
}

/* §6 — the hero opened with roughly 250px between the nav and the eyebrow,
 * pushing the headline below the fold at 1440x900. */
.hero {
  padding-block: var(--space-8) var(--space-10);
}

@media (min-width: 1024px) {
  .hero {
    padding-block: var(--space-10) var(--space-12);
  }
}

/* ======================================================================
 * REVISION-9 §2 — how Telyne compares
 *
 * A real <table>, because it is tabular data and screen readers should be
 * told so. Every state carries a glyph, a colour and a text label, so it
 * survives colour blindness and a screenshot in greyscale alike. On narrow
 * screens it becomes one card per row group rather than a horizontally
 * scrolling table, which is unusable on a phone.
 * ==================================================================== */

.cmp {
  margin-top: var(--space-10);
}

.cmp-t {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

.cmp-h {
  padding: var(--space-3) var(--space-4);
  /* Top, not bottom. The descriptor lines run to one, two and three lines, so
   * aligning the cells' bottoms pushed the four headings to three different
   * heights and the row read as ragged. Aligning the tops puts the headings on
   * one line and lets the descriptors hang to whatever length they need. */
  vertical-align: top;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-secondary);
  border-bottom: 1px solid var(--border);
}

.cmp-h--us {
  color: var(--brand-700);
  font-weight: 700;
}

/* The category each column stands for, so a reader can place their own
 * supplier without having to infer it from the ticks. */
.cmp-h-desc {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--text-muted);
}

/* Telyne is in development, and the notice bar and FAQ say so. An unmarked
 * column here would be the only unqualified claim on the page. */
.cmp-h-badge {
  display: inline-block;
  margin-left: var(--space-2);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--tint-brand);
  color: var(--brand-700);
  font-size: var(--text-micro);
  font-weight: 700;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  vertical-align: middle;
}

/* The coverage tally. It makes the span argument countable instead of asking
 * the reader to tot up ticks down four columns. */
.cmp-foot .cmp-rowlab,
.cmp-foot .cmp-c {
  border-top: 2px solid var(--border-strong);
  padding-top: var(--space-4);
  padding-bottom: var(--space-4);
  /* Only three of the four cells carry a partial line, so centring left the
   * single-line total sitting lower than the other three. */
  vertical-align: top;
}

.cmp-count {
  display: block;
  font-weight: 700;
  color: var(--text);
  /* "6 of 14" is one figure and must never break across lines. */
  white-space: nowrap;
}

.cmp-c--us .cmp-count {
  color: var(--brand-700);
}

/* Partials are counted separately rather than folded into the total. Rolling
 * them in would overstate a category; leaving them out silently would
 * understate one — the connectivity column is 2 covered and 6 partial, so the
 * bare total alone tells the reader the wrong thing. */
.cmp-count-part {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--text-muted);
}

.cmp-grouplab {
  padding: var(--space-6) var(--space-4) var(--space-2);
  font-size: var(--text-micro);
  font-weight: 700;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--on-azure);
}

.cmp-grouplab[data-tone="cyan"] { color: var(--on-cyan); }
.cmp-grouplab[data-tone="teal"] { color: var(--on-teal); }
.cmp-grouplab[data-tone="coral"] { color: var(--on-coral); }
.cmp-grouplab[data-tone="amber"] { color: var(--on-amber); }

.cmp-rowlab {
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text);
  border-top: 1px solid var(--border);
}

.cmp-c {
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border);
}

/* Telyne's column: tinted and a little heavier, so it reads as the answer
 * without shouting. */
.cmp-c--us,
.cmp-h--us {
  background: var(--tint-brand-08);
  border-left: 2px solid var(--brand-500);
  border-right: 2px solid var(--brand-500);
}

/* The tinted column is closed by the coverage row in the tfoot, which is now
 * the bottom of the table rather than the last tbody. */
.cmp-foot .cmp-c--us {
  border-bottom: 2px solid var(--brand-500);
}

.cmp-st {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-pill);
}

.cmp-st--yes { color: var(--on-green); background: var(--tint-green); }

/* "In part" was amber, the palette's warning hue, which made a partial
 * capability read as a caution against the category rather than as a neutral
 * fact about it. All three states are information, so this one is neutral:
 * present and legible, but carrying no verdict. It still sits a clear step
 * above "not offered", which is fainter again. */
.cmp-st--part { color: var(--on-neutral); background: var(--tint-neutral); }
.cmp-st--no { color: var(--text-muted); background: var(--bg-sunken); }

.cmp-key {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  margin-top: var(--space-6);
  padding: 0;
  list-style: none;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.cmp-key li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Required by §2 and always visible — never inside a disclosure. */
.cmp-caveat {
  margin-top: var(--space-6);
  max-width: 62ch;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.cmp-close {
  margin-top: var(--space-8);
  text-align: center;
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--text);
}

.cmp .sec-head-more {
  margin-top: var(--space-2);
}

/* --- narrow screens: one card per row group, never a scrolling table --- */
@media (max-width: 767px) {
  .cmp-t,
  .cmp-t tbody,
  .cmp-t tr,
  .cmp-t th,
  .cmp-t td {
    display: block;
  }

  .cmp-t thead {
    display: none;
  }

  .cmp-group {
    margin-top: var(--space-5);
    padding: var(--space-5);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-sm);
  }

  .cmp-grouprow {
    margin-bottom: var(--space-2);
  }

  .cmp-grouplab {
    padding: 0 0 var(--space-2);
  }

  .cmp-rowlab {
    padding: var(--space-4) 0 var(--space-2);
    border-top: 1px solid var(--border);
    font-weight: 600;
  }

  .cmp-group tr:first-of-type .cmp-rowlab,
  .cmp-grouprow + tr .cmp-rowlab {
    border-top: 0;
  }

  /* The column header travels with the cell, so a value is never orphaned
   * from the category it belongs to. */
  .cmp-c {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-2) 0;
    border-top: 0;
  }

  .cmp-c::before {
    content: attr(data-col);
    font-size: var(--text-sm);
    color: var(--text-secondary);
  }

  .cmp-c--us {
    margin-top: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-md);
    border-left: 2px solid var(--brand-500);
    border-right: 0;
  }

  .cmp-c--us::before {
    color: var(--brand-700);
    font-weight: 700;
  }

  .cmp-group:last-child .cmp-c--us,
  .cmp-foot .cmp-c--us {
    border-bottom: 0;
  }

  /* The coverage row becomes a card like every other group, so the tally is
   * not stranded as four loose lines under the last one. */
  .cmp-foot {
    display: block;
    margin-top: var(--space-5);
    padding: var(--space-5);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-sm);
  }

  .cmp-foot .cmp-rowlab {
    padding: 0 0 var(--space-2);
    border-top: 0;
    font-weight: 700;
  }

  /* Qualified with .cmp-t for the same reason as the spacer above: the plain
   * selector loses to `.cmp-t td { display: block }`, and without the flex the
   * category label ran straight into its own figure — "Communications
   * platforms6 of 14". */
  .cmp-t .cmp-foot .cmp-c {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    border-top: 0;
    padding-top: var(--space-2);
    padding-bottom: var(--space-2);
  }

  /* Two flex items, not three — the label and the figures. With the count and
   * its partial as separate items, space-between stranded the count in the
   * middle of the row against the label. */
  /* Stacked and right-aligned, not side by side. At 390 the card is 310px and
   * the longest label takes two lines of it, which left the figures so little
   * room that "6 of 14" broke after "of". */
  .cmp-countwrap {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    text-align: right;
  }

  .cmp-count-part {
    margin-top: 0;
    white-space: nowrap;
  }
}

/* Telyne now sits in the first data column, so the group heading row can no
 * longer span the label column and the categories in one cell — the tinted
 * column runs between them. The row is therefore three cells: the group label,
 * the tinted spacer that keeps Telyne's column unbroken, and a pad covering the
 * three category columns. */
.cmp-c--spacer,
.cmp-grouppad {
  border-top: 0;
}

@media (max-width: 767px) {
  /* Qualified with .cmp-t deliberately. `.cmp-t td { display: block }` above is
   * specificity 0,1,1 and an unqualified `.cmp-c--spacer` is 0,1,0, so the
   * plain selector lost and the spacer rendered as a stray 16px tinted chip
   * under every group heading — measured, not guessed. The same trap is why
   * `.cmp-c { display: flex }` never took effect on the body cells; that one is
   * a wider change and is raised in BACKLOG.md rather than fixed here. */
  .cmp-t .cmp-c--spacer,
  .cmp-t .cmp-grouppad {
    display: none;
  }
}

/* --- wide screens: stop the columns drifting apart ----------------------
 * The container maxes out at 1280, so the table is 1184px at both 1280 and
 * 1440 — measured, identical at both. That means "sparse at 1440" is equally
 * true at 1280, and a 1440-only rule would fix one width and leave the other
 * looking different for no reason. Applied from 1280 up.
 *
 * Before: label 281px and category columns 275 / 239 / 282px, each holding a
 * single 22px glyph. The columns are pulled in and the label column takes the
 * slack, which is the column that actually has words in it. */
@media (min-width: 1280px) {
  .cmp-corner,
  .cmp-rowlab {
    width: 30%;
  }

  .cmp-h,
  .cmp-c {
    width: 17.5%;
  }
}

/* REVISION-9 §3 — hero balance, measured rather than assumed.
 *
 * The illustration is square, so its height is its column width: a wide
 * column makes a tall illustration, and no amount of vertical tuning inside
 * it changes that. The lever is the column, not the artwork — which also
 * keeps the illustration filling its column, so it never shrinks into a
 * pool of empty space.
 *
 * At 5fr/7fr the illustration stood 663px against 428px of text, 35% apart.
 * 7.5fr/4.5fr brings them together, and the wider text column is also what
 * lets the headline hold display-l unwrapped from 1280.
 *
 * As the gate measures it: 442px of text against 500-506px of illustration at
 * 1280 and 1440, so 11.5-12.7% apart, inside the 15% cap.
 *
 * An earlier draft of this comment claimed 426px and 3.8%. Nothing reproduces
 * that. The gate takes the extent of every painted descendant, which includes
 * the ambient glow overhanging the artwork, so it reads larger than the SVG
 * box — and because that glow is still animating when the measurement is
 * taken, the illustration figure moves by up to 10px between runs. Read these
 * as a band, not a fixed number, and note the headroom to 15% is only about
 * 2-3 points with roughly 1.5 points of run-to-run movement in it.
 */
@media (min-width: 1280px) {
  .hero-grid {
    grid-template-columns: minmax(0, 7.5fr) minmax(0, 4.5fr);
  }

  .hero h1 {
    font-size: var(--text-display-l);
  }
}

/* ---------------------------------------------------------------------------
 * THREE INSTANCES THAT PUT TEXT IN A BRAND ACCENT, brought to WCAG AA.
 *
 * Founder ruling, 7 September 2026: --cap-cyan and --cap-green keep their hex
 * because they are brand and mostly decorative. Where either genuinely carries
 * prose, the INSTANCE changes, not the token. These are those instances.
 *
 * WHY THEY ARE HERE AND NOT IN capability.css. That file is generated by
 * tools/extract-capability-css.js from the approved eSIM reference and is never
 * edited by hand, and it loads AFTER this one. So an override has to win on
 * specificity rather than on order. The doubled class is deliberate and is the
 * whole trick: .cp-lcarc.cp-lcarc is (0,3,0) against capability.css's (0,2,0),
 * and it changes nothing else about what is selected. The same pattern already
 * carries .cp-wrap.cp-wrap--narrow.
 *
 * Type weight was considered first, per the ruling, and does not reach: large
 * text still needs 3.0:1 and two of the three measured below even that.
 * ------------------------------------------------------------------------ */

/* "RESUME" on white — was 2.37:1, now 4.63:1. */
.cp-capability .cp-lcarc.cp-lcarc {
  color: var(--cap-cyan-text);
}

/* "Live" on the green tint — was 2.96:1, now 4.60:1. */
.cp-capability .cp-state.cp-state--live {
  color: var(--cap-green-700);
}

/* The finished clock step: white "04" on green — was 3.24:1, now 5.04:1.
 * The text is white and stays white; the background is what moves. */
.cp-capability .cp-ct.cp-ct.cp-done {
  background: var(--cap-green-700);
}

/* The clock's columns must be allowed to shrink.
 *
 * .cp-clock is a grid of repeat(4, 1fr), and a grid item defaults to
 * min-width:auto — so a track cannot go narrower than its content's
 * min-content width. One unbreakable string therefore widens the track and
 * pushes the whole grid past .cp-clocksec, which hides overflow, and the text
 * at the right edge is clipped.
 *
 * Measured on capabilities/documentation at 320: four elements sat 3px past
 * the section's right edge. It surfaced when heading tails were bound with a
 * non-breaking space, but the binding only revealed it — any long word does
 * the same, and would have done it to a future heading nobody was watching.
 *
 * min-width:0 is the standard remedy and is already what .codepanel uses.
 * Doubled class to out-specify capability.css, which is generated and loads
 * after this file. */
.cp-capability .cp-clock.cp-clock > div {
  min-width: 0;
}

/* Code strings on the dark panel — was 2.38:1 against --ink-800, now 4.62:1.
 *
 * --on-teal keeps its hex: it is also .cmp-grouplab[data-tone="teal"], which
 * sits on light and is fine there. Only the instance on ink moves, which is
 * the same shape of fix as .cp-lcarc and .cp-state--live above. */
.codebox .s {
  color: var(--on-teal-ink);
}

/* The line under the catalogue legend that says what the chips mean.
 *
 * The legend was two coloured chips and two words, and a reader seeing amber
 * "Approval" against a capability inferred a gate standing in front of them.
 * With point-of-touch verification that inference is wrong: the sandbox is open
 * to anyone, verification applies only to going live, and it is asked once per
 * account. A label cannot carry that; a sentence can. */
.caplegend-note {
  max-width: 62ch;
  margin: var(--space-3) auto 0;
  text-align: center;
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-secondary);
}

/* ------------------------------------------------------------------ *
 * The account screen — /app
 * ------------------------------------------------------------------ *
 *
 * The capability list is a <dl class="cp-spec">, and each term is a link to
 * that capability's page. A spec list normally holds static text, so its <dt>
 * inherits a 20px line box — which is fine for a word and four pixels short
 * for something a finger has to hit. qa-tap measured all nineteen at 20px tall and
 * failed the page.
 *
 * So the links, and only the links, become their own 24px tap target. The
 * enclosing <dt> is untouched, so every other spec list on the site keeps the
 * rhythm it has.
 *
 * SCOPED BY A CLASS OF ITS OWN, not by the section id. This was written as
 * #capabilities first — and design/index.html carries that same id, so the rule
 * reached a page it was never measured against. A selector that can touch a
 * page nobody re-gated is how a layout regression arrives silently.
 */
.app-caps dt a {
  display: inline-block;
  min-height: 24px;
  line-height: 24px;
}

/* ==================================================================== *
 * THE DESIGN PASS — 21 September 2026
 * ==================================================================== *
 *
 * Founder review: pages other than eSIM look like thinner copies of eSIM
 * rather than pages designed for their own subject. Two faults were named on
 * /white-label and both turned out to be one cause with many instances.
 *
 * eSIM IS THE QUALITY REFERENCE AND WAS USED AS A TEMPLATE. Its layouts were
 * sized for its content — four audience cards, a big "180+" beside a globe —
 * and every page built from the same classes inherited those sizes with less
 * to put in them. Nothing overflows and nothing is clipped, so no gate saw it.
 * It can only be seen.
 *
 * These overrides live here rather than in capability.css because that file is
 * GENERATED from the approved reference and an edit there is discarded by the
 * next extraction.
 */

/* ---- 1. grids that hold fewer items than they have columns ------------
 *
 * MEASURED, not guessed: 16 grid instances across 36 pages put fewer children
 * into a fixed repeat(N,1fr) than it has columns.
 *
 *   .cp-who      repeat(4)  five pages put 3 in it   75% of the width used
 *   .cp-nolist   repeat(4)  one page 2, six pages 3  50% and 75%
 *   .cp-rel      repeat(3)  three pages put 2 in it  67%
 *
 * auto-fit COLLAPSES the empty tracks and stretches what is left, so three
 * cards become three wide cards rather than three narrow ones and a hole. The
 * minimum is what stops four items becoming one long row on a phone; below it
 * the grid wraps as it did before.
 *
 * THE DOUBLED CLASS IS NOT A TYPO. capability.css is linked AFTER this file,
 * so a selector of equal specificity here loses the cascade — which is how the
 * first version of this block changed nothing while appearing to work. The
 * repeated class raises specificity by one without changing what is matched.
 *
 * THE REFERENCE PAGE IS INCLUDED. It was excluded on a misreading: the founder
 * fenced eSIM off from being COPIED, not from being fixed. Its two-item
 * .cp-nolist used 50% of a full-width dark bar, which is the same fault as
 * everywhere else, and a reference page with a hole in it is a worse reference.
 * The per-page hook stays — it is generally useful and cost nothing. */
.cp-capability .cp-who.cp-who,
.cp-capability .cp-nolist.cp-nolist,
.cp-capability .cp-rel.cp-rel {
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}

/* ---- 2. text sized for a number it does not have ----------------------
 *
 * .cp-covertext p is capped at 22ch. On eSIM that is right: the paragraph sits
 * under a 60px "180+" beside a globe, and a wide line there would fight the
 * number. On /white-label the same panel carries sixty words and no number, so
 * 22ch squeezes them into a ribbon down the left with the panel empty beside
 * it — the founder's second finding.
 *
 * :has() asks the only question that matters: is there a number to sit under?
 * Both engines the gates run support it. */
.cp-capability .cp-covertext:not(:has(b)) p {
  max-width: 62ch;
  font-size: 15.5px;
  line-height: 1.6;
}

/* A panel with no number has no reason to reserve the right half for a figure
 * that bleeds in from outside it. */
.cp-capability .cp-covermap:not(:has(.cp-covertext b)) {
  padding: 28px 30px 30px;
}

/* ---- 3. interactive affordance, per DESIGN-AUTHORITY §13 --------------
 *
 * RESTRAINED ON PURPOSE. §13 permits interactive animation and reserves the
 * animation LANGUAGE — easing, duration, what moves — to the founder. What is
 * here is affordance rather than expression: a card that can be pointed at
 * says so, and a card that cannot does not move. Nothing animates on load,
 * nothing animates while a reader waits for anything, and no motion carries
 * meaning that is not also carried by the border and the shadow.
 *
 * Tokens, not literals, so the language can be changed in one place when it is
 * settled. */
@media (hover: hover) and (pointer: fine) {
  .cp-capability .cp-who > div,
  .cp-capability .cp-rel > a,
  .cp-capability .cp-nolist > div {
    transition: transform var(--duration-base) var(--ease-out),
                box-shadow var(--duration-base) var(--ease-out),
                border-color var(--duration-fast) var(--ease-out);
  }
  .cp-capability .cp-who > div:hover,
  .cp-capability .cp-rel > a:hover {
    transform: translateY(-3px);
    box-shadow: 0 20px 40px -26px rgba(8, 25, 46, 0.62);
    border-color: var(--cap-brand-200, var(--cap-line));
  }
}

/* Focus is not hover. A keyboard user gets the same affordance, and gets it
 * whether or not they have a pointer — which is why this sits outside the
 * hover query. */
.cp-capability .cp-rel > a:focus-visible {
  transform: translateY(-3px);
  box-shadow: 0 20px 40px -26px rgba(8, 25, 46, 0.62);
}

/* ---- 4. NO EXCEPTION -------------------------------------------------
 *
 * DESIGN-AUTHORITY §13: prefers-reduced-motion is respected everywhere, with
 * no exception. Not "mostly", not "except the good bits". qa-page.js asserts
 * nothing animates for longer than 1ms under this query on every page carrying
 * a form, and it is the gate this block has to satisfy rather than argue with.
 *
 * The visual affordance stays — border and shadow still change — because the
 * setting is about MOTION, not about removing the feedback a person needs to
 * know a card is interactive. */
@media (prefers-reduced-motion: reduce) {
  .cp-capability .cp-who > div,
  .cp-capability .cp-rel > a,
  .cp-capability .cp-nolist > div {
    transition: none;
  }
  .cp-capability .cp-who > div:hover,
  .cp-capability .cp-rel > a:hover,
  .cp-capability .cp-rel > a:focus-visible {
    transform: none;
  }
}

/* ---- 5. hero figures ------------------------------------------------
 *
 * Founder ruling, 22 September 2026: treatment A, the thin-stroke figure,
 * rolled out to every hero whose ink stopped short. Nineteen of thirty-six
 * did, and the worst stopped at 45% — the right half of a full-width band
 * doing nothing.
 *
 * A FIGURE IS A CLAIM. What each one may depict is decided in
 * tools/hero-figures.js, where every entry records what a reader will take
 * from it and the confirmed fact that entitles us to draw it. This block is
 * only the geometry.
 *
 * The figure is the SECOND COLUMN, slightly narrower than the text, and it
 * centres against it. It collapses to a row of its own below 900px rather than
 * shrinking to a stamp — a diagram that has to be squinted at is worse than no
 * diagram, and on a phone the text is what matters.
 */
.cp-capability .cp-hero--fig .cp-wrap,
.cp-capability .hero--fig .wrap {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.82fr);
  gap: 48px;
  align-items: center;
}

.cp-capability .cp-heromain,
.cp-capability .heromain { min-width: 0; }

.cp-capability .cp-herofig {
  min-width: 0;
  /* The drawing is decoration that repeats the h1. It is aria-hidden at the
   * svg, and this keeps it out of the way of a long heading too. */
  align-self: center;
}

.cp-capability .cp-herofig svg {
  width: 100%;
  height: auto;
  display: block;
  max-width: 460px;
  margin-left: auto;
}

/* The capability pages put the figure in the existing hero grid's second
 * column, which already has its own rules — only the figure itself needs
 * sizing there. */
.cp-capability .herogrid > .cp-herofig { align-self: center; }

@media (max-width: 900px) {
  .cp-capability .cp-hero--fig .cp-wrap,
  .cp-capability .hero--fig .wrap {
    grid-template-columns: 1fr;
    gap: 26px;
  }
  .cp-capability .cp-herofig svg { max-width: 420px; margin: 0 auto; }
}

/* Below a phone's width a 420px diagram is a stamp. Hide it rather than shrink
 * it: the h1 and the lede carry the same meaning, and the figure was never
 * load-bearing. It is aria-hidden already, so nothing is lost to a reader
 * using a screen reader either. */
@media (max-width: 560px) {
  .cp-capability .cp-herofig { display: none; }
}

/* ==================================================================
   THE SIGN-IN PAGE
   ==================================================================

   One job, one card, nothing else on the screen. The sign-in form used
   to be the fourth thing down /app — under a hero, a breadcrumb and a
   lede explaining what an account is, on the page a member lands on
   once they are already in.

   So this page has no hero, no breadcrumb, no capture band (build.js
   honours `noCapture` in the front matter) and exactly one button. The
   card is deliberately narrow: a single field reads as a single field at
   26rem and as an empty expanse at full width.

   Centred VERTICALLY as well as horizontally, which is the thing that
   makes a page like this feel finished. min-height is computed from the
   viewport less the chrome the layout puts above and below it, so the
   card sits in the optical centre of what is left rather than being
   pushed below the fold on a laptop.
   ================================================================== */
.signin {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-5);
  /* FILLS THE VIEWPORT BELOW THE CHROME, so the card centres in what a person
     can actually see and the footer starts below the fold instead of leaving a
     gap above itself.

     The 4.5rem is the notice bar plus the nav, measured at 69px at every width
     this page is checked at — it does not change with width, because the nav is
     a fixed height. Subtracting it is what stops 100svh pushing the card down
     by exactly the height of the chrome, which is the fault the previous
     min(68vh, 40rem) was avoiding by being too short instead.

     svh and not vh: on a phone, vh is the viewport WITHOUT the browser's own
     chrome, so 100vh overflows and the page scrolls when it should not. */
  min-height: calc(100svh - 4.5rem);
  padding: var(--space-10) var(--space-4);
}

.signin-card {
  width: 100%;
  max-width: 26rem;
  background: var(--grey-0);
  border: 1px solid var(--grey-200);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  box-shadow: var(--shadow-sm);
}

.signin-h {
  margin: 0 0 var(--space-2);
  font-size: var(--text-h1);
  line-height: 1.2;
  letter-spacing: -0.015em;
  color: var(--grey-800);
  text-wrap: balance;
}

.signin-sub {
  margin: 0 0 var(--space-5);
  font-size: var(--text-body);
  line-height: 1.6;
  color: var(--grey-600);
}

.signin-form { margin: 0; }

/* Full width, because it is the only action on the page and a button that
   does not fill its field looks like one of several. */
.signin-go {
  width: 100%;
  margin-top: var(--space-4);
}

.signin-note {
  margin: var(--space-5) 0 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--grey-200);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--grey-600);
}

/* Outside the card, so it reads as a way out rather than as part of the
   form. */
.signin-foot {
  margin: 0;
  font-size: var(--text-body);
  color: var(--grey-600);
  text-align: center;
}

@media (max-width: 560px) {
  /* Still centred, but with less of the viewport claimed: a phone keyboard
     covers the lower half of the screen, and a field centred in the full height
     ends up underneath it. */
  .signin { min-height: calc(100svh - 7rem); padding: var(--space-6) var(--space-4); }
  .signin-card { padding: var(--space-5); }
}

/* ===================================================================
   THE API REFERENCE  —  /docs/
   ===================================================================

   The page is GENERATED by tools/gen-docs.js from worker/src/openapi.js, so
   these rules describe a structure nobody types: nineteen classes, every one of
   them emitted by that generator. tools/check-classes.js asserts the other
   direction — that every class in the markup has a rule here — which is what
   stops the generator growing a class with no styling.

   EVERY SIZE IS A TOKEN. The same rule as everywhere else: no hex, no typed
   step. The page is dense by nature — fourteen operations, four objects and
   twenty-five error codes — so the work the CSS does is mostly rhythm and
   making a table readable at 390px without a horizontal scroll on the page
   itself.
   =================================================================== */

.docs {
  max-width: var(--container, 1280px);
  margin: 0 auto;
  padding: var(--space-10) var(--space-5) var(--space-16);
}

.docs-h { font-size: var(--text-display-s); line-height: 1.1; margin: 0 0 var(--space-4); }

.docs-lede {
  font-size: var(--text-lg);
  color: var(--text-secondary);
  max-width: 68ch;
  margin: 0 0 var(--space-8);
}

/* ---- on this page ------------------------------------------------- *
 * A flat row of links rather than a nested tree. The page has seven sections
 * and a tree of fourteen operations would be a second navigation to read. */
.docs-toc {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-10);
  padding: var(--space-4) 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.docs-toc a {
  /* 44px, the floor DESIGN-AUTHORITY §12 sets and qa-tap.js measures. These are
   * links in a row rather than in prose, so the inline exemption does not apply
   * to them and they carry the full target. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  text-decoration: none;
  color: var(--text-secondary);
}
.docs-toc a:hover { color: var(--brand-600); border-color: var(--brand-600); }

/* ---- a section --------------------------------------------------- */
.docs-sec { margin: 0 0 var(--space-16); scroll-margin-top: var(--space-16); }
.docs-sec > h2 {
  font-size: var(--text-h1);
  line-height: 1.2;
  margin: 0 0 var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}
.docs-sec > h3 { font-size: var(--text-h2); margin: var(--space-8) 0 var(--space-3); }

.docs-count { font-size: var(--text-sm); color: var(--text-secondary); margin: 0 0 var(--space-6); }

/* ---- one operation ----------------------------------------------- */
.docs-op,
.docs-obj {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  margin: 0 0 var(--space-5);
  scroll-margin-top: var(--space-16);
}
.docs-op-h { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center;
  margin: 0 0 var(--space-3); font-size: var(--text-h3); }
.docs-obj > h3 { margin: 0 0 var(--space-3); font-size: var(--text-h3); }

/* THE VERB IS NOT CARRIED BY COLOUR ALONE. It is a word, in a bordered pill, so
 * it reads as GET or POST whatever a reader can or cannot see. */
.docs-verb {
  display: inline-block;
  padding: var(--space-05) var(--space-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xs);
  background: var(--grey-100);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  color: var(--text-primary, var(--grey-800));
}
.docs-path { font-family: var(--font-mono); font-size: var(--text-body); word-break: break-all; }

.docs-sum { margin: 0 0 var(--space-3); color: var(--text-secondary); }
.docs-op h4,
.docs-obj h4 { font-size: var(--text-sm); letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--text-secondary); margin: var(--space-5) 0 var(--space-2); }

/* ---- tables ------------------------------------------------------ *
 * SCROLLABLE IN THEMSELVES, not scrolling the page. A four-column table of
 * parameters cannot be made to fit 390px honestly, and a page that scrolls
 * sideways is the fault qa-page.js fails a build for. */
.docs-table {
  display: block;
  overflow-x: auto;
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
  margin: 0 0 var(--space-4);
}
.docs-table th,
.docs-table td {
  text-align: left;
  vertical-align: top;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
}
.docs-table th {
  font-size: var(--text-xs);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-secondary);
  white-space: nowrap;
}
.docs-table tbody tr:last-child td { border-bottom: 0; }
.docs-k { white-space: nowrap; }
.docs-k code { font-family: var(--font-mono); }
.docs-t { color: var(--text-secondary); }
.docs-d { color: var(--text-secondary); }
.docs-op .docs-d,
.docs-obj .docs-d { max-width: 72ch; }

/* REQUIRED IS A WORD. A colour or an asterisk would be the only carrier. */
.docs-req {
  display: inline-block;
  margin-left: var(--space-1);
  padding: 0 var(--space-1);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xs);
  font-size: var(--text-micro);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* ---- lists ------------------------------------------------------- */
.docs-list { margin: 0 0 var(--space-4); padding-left: var(--space-5); color: var(--text-secondary);
  font-size: var(--text-sm); }
.docs-list li { margin: 0 0 var(--space-1); }
.docs-list code,
.docs-cols code { font-family: var(--font-mono); }

/* The event types: a plain set of names, in columns so nine of them are one
   glance rather than a column of nine. */
.docs-cols {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: var(--space-2);
  font-size: var(--text-sm);
}

.docs-note { font-size: var(--text-sm); color: var(--text-secondary); margin: 0 0 var(--space-4); }

@media (max-width: 640px) {
  .docs { padding: var(--space-8) var(--space-4) var(--space-12); }
  .docs-op,
  .docs-obj { padding: var(--space-4); }
}

/* A LINK IN A TABLE CELL IS STILL A TAP TARGET.
 *
 * DESIGN-AUTHORITY §12 sets 24px for a link that is not inline in prose, and
 * qa-tap.js measures it in both engines at 390. The error table's twenty-five
 * links, and the one to the specification, rendered 17px tall — the line box of
 * 13px text — because a cell is not prose and the inline exemption does not
 * apply to them. inline-flex rather than inline-block, so the text centres in the
 * box instead of sitting at its top. */
.docs-table a,
.docs-cols a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}
