/* ==========================================================================
   Ringplane — Components
   Section and component stylesheet. Every value below resolves to a token
   defined in tokens.css. No hardcoded color, space, radius, or timing.
   ========================================================================== */

/* ===== 1. Header ===== */

.hdr {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: var(--z-header);
  background: transparent;
  border-bottom: 1px solid transparent;
  transition:
    background-color var(--d-3) var(--ease-out),
    border-color var(--d-3) var(--ease-out);
}
.hdr.is-stuck {
  /* 88% let large display type ghost through behind the bar. */
  background: color-mix(in srgb, var(--bg) 97%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border-bottom-color: var(--line);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  /* Without blur support, translucency alone is not enough to separate the
     bar from scrolling content. */
  .hdr.is-stuck { background: var(--bg); }
}

.hdr__in {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-6);
  padding-block: var(--s-4);
}

.hdr__brand {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  color: var(--ink);
}
.hdr__brand .mark { color: var(--ring); }
.hdr__name {
  font-family: var(--font-data);
  font-size: var(--t-small);
  letter-spacing: var(--track-mono);
  font-weight: 500;
}

.mark {
  display: inline-flex;
  width: 40px;
  height: 40px;
  flex: none;
}
.mark svg { width: 100%; height: 100%; }

.hdr__nav { display: none; }
@media (min-width: 900px) {
  .hdr__nav { display: block; }
  .hdr__nav ul {
    display: flex;
    align-items: center;
    gap: var(--s-6);
  }
  .hdr__nav a {
    position: relative;
    font-family: var(--font-data);
    font-size: var(--t-nano);
    letter-spacing: var(--track-label);
    text-transform: uppercase;
    color: var(--ink-3);
    padding-block: var(--s-2);
    transition: color var(--d-2) var(--ease-out);
  }
  .hdr__nav a:hover { color: var(--ink); }
  .hdr__nav a[data-navlink].is-active { color: var(--signal); }
  .hdr__nav a[data-navlink].is-active::after {
    content: "";
    position: absolute;
    left: 0; right: 0; bottom: -1px;
    height: 1px;
    background: var(--signal);
  }
}

.hdr__act {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}
.hdr__act .btn--ghost.btn--sm { padding-inline: var(--s-3); }

.hdr__progress {
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 2px;
  background: transparent;
  overflow: hidden;
}
.hdr__progress i {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--signal);
  transform: scaleX(0);
  transform-origin: left;
  will-change: transform;
}

/* ===== 2. Buttons ===== */

.btn {
  --btn-pad-y: var(--s-3);
  --btn-pad-x: var(--s-5);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--btn-pad-y) var(--btn-pad-x);
  border-radius: var(--r-2);
  font-family: var(--font-text);
  font-size: var(--t-small);
  font-weight: 500;
  letter-spacing: 0.01em;
  white-space: nowrap;
  cursor: pointer;
  border: 1px solid var(--signal-dim);
  background: var(--signal-a-08);
  color: var(--signal-2);
  transition:
    background-color var(--d-2) var(--ease-out),
    border-color var(--d-2) var(--ease-out),
    color var(--d-2) var(--ease-out),
    transform var(--d-1) var(--ease-out);
}
.btn:hover {
  background: var(--signal-a-16);
  border-color: var(--signal);
  color: var(--signal-2);
}
.btn:active { transform: translateY(1px); }

.btn--ghost {
  background: transparent;
  border-color: var(--line-2);
  color: var(--ink-2);
}
.btn--ghost:hover {
  background: var(--a-04);
  border-color: var(--line-3);
  color: var(--ink);
}

.btn--sm {
  --btn-pad-y: var(--s-2);
  --btn-pad-x: var(--s-4);
  font-size: var(--t-micro);
}

.btn--lg {
  --btn-pad-y: var(--s-4);
  --btn-pad-x: var(--s-6);
  font-size: var(--t-body);
}

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

/* ===== 3. Hero ===== */

.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
  background: var(--bg);
  padding-block: calc(var(--s-7) + var(--s-5)) var(--s-5);
}

.hero__canvas {
  position: absolute;
  inset: 0;
  z-index: var(--z-canvas);
  pointer-events: none;
  width: 100%;
  height: 100%;
}

.hero__veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    /* Horizontal scrim over the text column. Once the field resolves into a
       plane it sweeps across the copy, so the left side needs to stay dark
       enough to read against while the right stays clear for the artwork. */
    linear-gradient(90deg,
      color-mix(in srgb, var(--bg) 92%, transparent) 0%,
      color-mix(in srgb, var(--bg) 88%, transparent) 30%,
      color-mix(in srgb, var(--bg) 74%, transparent) 48%,
      color-mix(in srgb, var(--bg) 40%, transparent) 62%,
      color-mix(in srgb, var(--bg) 14%, transparent) 74%,
      transparent 88%),
    linear-gradient(180deg,
      color-mix(in srgb, var(--bg) 55%, transparent) 0%,
      color-mix(in srgb, var(--bg) 20%, transparent) 38%,
      color-mix(in srgb, var(--bg) 55%, transparent) 72%,
      var(--bg) 100%);
}

/* Narrow-viewport slot for the orbital field, between the CTAs and the
   telemetry readout. Collapsed to nothing on desktop, where the canvas is a
   full-bleed backdrop behind a side-by-side column and works as designed. */
.hero__art { display: none; }

@media (max-width: 899px) {
  /* Narrow viewports have no side-by-side column, so a full-bleed canvas has
     nowhere to be that is not behind text. Two earlier attempts failed the
     same way for the same reason: BOTH positioned the field by measuring the
     copy and hoping the two would not collide.

     1. Full-bleed + scrim: darkened exactly where the particle bloom peaked.
     2. Push the copy down (--hero-art-band on .hero__in) and re-frame the
        field into the gap: the gap and the field were computed separately,
        so any change to type, wrapping or viewport height slid them apart —
        which is precisely what happened, and the field ended up behind the
        lead paragraph again.

     Now the band is an IN-FLOW ELEMENT (.hero__art) and the canvas is
     clipped to its measured box by main.js. Overlap is no longer something
     to be tuned and verified; it is structurally impossible, because the
     copy is laid out around the band rather than beside it. The scrim only
     has to protect the copy, so it can be a flat wash with a hole cut in it.
     ------------------------------------------------------------------- */
  .hero__art {
    display: block;
    height: var(--hero-art-h);
    /* Tight: the hero's content has to fit a 700-850px viewport WITH the band
       added, and every step here is spent twice (once above, once below).
       Measured at 390x844: the untightened rhythm needed 1052px of content
       and pushed the band itself below the fold, which is the one thing the
       band exists to prevent. */
    margin-block: var(--s-4) var(--s-4);
  }


  .hero__veil {
    /* --art-top/--art-h are written by main.js from the slot's real box.
       Transparent across the band, solid everywhere else. Fallbacks keep the
       copy readable during the first frame, before JS has measured. */
    --art-t: var(--art-top, 62%);
    --art-b: calc(var(--art-top, 62%) + var(--art-h, 0px));
    background:
      linear-gradient(180deg,
        color-mix(in srgb, var(--bg) 88%, transparent) 0%,
        color-mix(in srgb, var(--bg) 88%, transparent) calc(var(--art-t) - 1.5rem),
        transparent calc(var(--art-t) + 0.5rem),
        transparent calc(var(--art-b) - 0.5rem),
        color-mix(in srgb, var(--bg) 88%, transparent) calc(var(--art-b) + 1.5rem),
        color-mix(in srgb, var(--bg) 92%, transparent) 100%);
  }

  /* The canvas is clipped to the slot instead of filling the hero. Inset
     values come from main.js; the fallback is a harmless full-bleed. */
  .hero__canvas {
    top: var(--art-top, 0px);
    height: var(--art-h, 100%);
    bottom: auto;
  }
}

.hero__in {
  position: relative;
  z-index: var(--z-content);
  /* The hero's content must fit the fold regardless of how the copy wraps.
     Padding derived from the viewport (rather than fixed steps) keeps the
     telemetry row seated on short laptop displays without re-tuning by hand
     every time a line length changes. */
  /* Bottom padding RESERVES the scroll cue's strip. The cue is absolutely
     positioned against .hero, so it is out of flow and the telemetry readout
     happily grew underneath it — on a 390px viewport the readout's wrapped
     third cell ("state / RESOLVED") and the centred "scroll to resolve"
     label collided outright. Reserving the space is what keeps them apart at
     every height; the cue itself stays pinned to the hero's bottom edge. */
  padding-block: var(--hero-in-pad-top)
                 calc(clamp(var(--s-5), 5vh, var(--s-7)) + var(--hero-cue-space));
  width: 100%;
}

.hero__kicker {
  margin-bottom: var(--s-4);
}

.hero__h {
  margin-bottom: var(--s-4);
  max-width: 15ch;
}
.hero__line {
  display: block;
}
/* A slide-up mask (overflow:hidden + translateY) clips the ascenders and
   descenders of a high-contrast display serif, and leaves the text invisible
   if the animation never runs. A non-clipping rise+fade is robust: overflow
   stays visible, and the resting state is fully opaque text. */
.hero__line > span {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.28em);
  animation: hero-line-in var(--d-4) var(--ease-out) forwards;
  animation-delay: calc(var(--i, 0) * 80ms + 60ms);
}
/* Set via attribute, not an inline style, so the CSP needs no
   'unsafe-inline' in style-src. */
.hero__line[data-i="0"] { --i: 0; }
.hero__line[data-i="1"] { --i: 1; }
.hero__line[data-i="2"] { --i: 2; }
.hero__line[data-i="3"] { --i: 3; }
@keyframes hero-line-in {
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .hero__line > span {
    /* opacity MUST be restored here: the animated state starts at 0, so
       disabling the animation without this leaves the headline invisible. */
    opacity: 1;
    transform: none;
    animation: none;
  }
}

.hero__lead {
  margin-bottom: var(--s-5);
  /* Shorter measure: better reading rhythm, and the line ends stop reaching
     into the densest part of the resolved plane. */
  max-width: 46ch;
}

.hero__cta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  /* 6rem here pushed the telemetry readout below the fold on a 900px-tall
     laptop. The readout is part of the hero's argument, not a footnote. */
  margin-bottom: var(--s-7);
}

/* Mobile hero rhythm, compressed to buy the art band its height.
   MUST live after the base .hero__* rules above: an identical-specificity
   media block placed EARLIER loses the cascade and silently does nothing.
   That mistake was made three times in this file's history, each time
   looking like a measurement failure rather than an ordering one.
   Measured at 390x844: the base rhythm needed 1052px of hero content and
   pushed the art band itself below the fold, which is the one outcome the
   band exists to prevent. */
@media (max-width: 899px) {
  .hero__kicker { margin-bottom: var(--s-3); }
  .hero__h { margin-bottom: var(--s-3); }
  .hero__lead { margin-bottom: var(--s-4); }
  .hero__cta { margin-bottom: var(--s-4); gap: var(--s-3); }
}

.readout {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  border-top: 1px solid var(--line);
  max-width: 44rem;
}
.readout__cell {
  flex: 1 1 33%;
  min-width: 9rem;
  padding: var(--s-4) var(--s-5) 0 0;
  border-left: 1px solid var(--line);
  padding-left: var(--s-5);
}
.readout__cell:first-child { border-left: 0; padding-left: 0; }

.readout__v {
  display: block;
  margin-top: var(--s-2);
  font-size: var(--t-display-3);
  font-size: clamp(1.35rem, 1.1rem + 1vw, 1.9rem);
  color: var(--ink);
  letter-spacing: -0.01em;
}
.readout__hint {
  margin-top: var(--s-1);
  color: var(--ink-4);
  font-size: var(--t-nano);
}

/* The three telemetry cells are ONE READING of the same system — inclination
   and alignment are the inputs, state is their conclusion. A 9rem min-width
   wrapped "state" onto a second row on a 390px viewport, which read as a
   detached card sitting under "inclination" rather than as the third column
   of a single instrument. Below 900px the row is therefore forced to stay a
   row: min-width drops, the label/value type steps down, and the gutters
   tighten. Three columns at 320px is tight but legible, and keeping the
   relationship intact is worth more than the extra few px per cell. */
@media (max-width: 899px) {
  .readout { flex-wrap: nowrap; }
  .readout__cell {
    flex: 1 1 0;
    min-width: 0;
    padding-right: var(--s-3);
    padding-left: var(--s-3);
  }
  .readout__cell:first-child { padding-left: 0; }
  .readout__cell:last-child { padding-right: 0; }
  .readout__v { font-size: clamp(0.95rem, 0.62rem + 2.1vw, 1.5rem); }
  .readout__hint { font-size: 0.56rem; letter-spacing: 0.02em; }
  .readout .label { font-size: 0.56rem; letter-spacing: 0.06em; }
}

/* Reserved strip for the cue, tuned to the cue's measured height (~64px:
   label + 12px gap + 34px rule) minus the padding it already sits inside.
   Kept as tight as possible — every pixel here grows the hero, and the
   telemetry readout must stay as close to the fold as it can. */
.hero {
  --hero-cue-space: var(--s-6);
  /* Height of the in-flow .hero__art slot. Zero on desktop, where the canvas
     is a full-bleed backdrop beside the copy column. */
  --hero-art-h: 0px;
  --hero-in-pad-top: clamp(var(--s-7), 9vh, var(--s-9));
}
@media (max-width: 899px) {
  /* The readout is a single forced row now, so the cue has less to clear
     than when "state" wrapped to a second row. Still a full step: measured
     at 320px a smaller reservation left only 4.2px between the readout and
     the cue, re-creating the collision this variable exists to prevent. */
  .hero { --hero-cue-space: var(--s-8); }
  /* The field's own band. Viewport-relative so it scales with the device
     instead of eating a fixed slab on small phones. Because the slot is in
     flow, this height is reserved by layout and cannot be encroached on. */
  .hero {
    --hero-art-h: clamp(120px, 17vh, 176px);
    --hero-in-pad-top: clamp(var(--s-5), 3vh, var(--s-6));
  }
  /* The band pushes total hero content past a phone viewport, so the hero
     stops trying to be exactly one screen tall on mobile. min-height is
     dropped to auto: forcing 100svh while the content needs more only adds
     padding, it does not make anything fit. The scroll cue still marks the
     boundary, and content is what sets the height. */
  .hero {
    min-height: 0;
    padding-block: calc(var(--s-7) + var(--s-3)) var(--s-4);
  }
}

.hero__scroll {
  position: absolute;
  left: 50%;
  bottom: var(--s-6);
  z-index: var(--z-content);
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-3);
  color: var(--ink-4);
  font-size: var(--t-nano);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}
.hero__scroll i {
  display: block;
  width: 1px;
  height: 34px;
  background: linear-gradient(180deg, var(--ink-4), transparent);
  position: relative;
  overflow: hidden;
}
.hero__scroll i::after {
  content: "";
  position: absolute;
  left: 0; top: -100%;
  width: 100%; height: 100%;
  background: linear-gradient(180deg, transparent, var(--signal), transparent);
  animation: hero-scroll-drip var(--d-5) var(--ease-in-out) infinite;
  animation-duration: 2.2s;
}
@keyframes hero-scroll-drip {
  to { top: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .hero__scroll i::after { animation: none; display: none; }
}

@media (min-width: 640px) {
  /* Viewport-relative, not a fixed 8rem: a flat step overflowed the fold on
     900px-tall laptops and pushed the telemetry row out of view. */
  /* Must re-add --hero-cue-space: this overrides the base padding-block, and
     dropping the reservation here is what left the desktop readout and the
     scroll cue overlapping by ~9px while mobile was already fixed. */
  .hero__in {
    padding-block: var(--hero-in-pad-top)
                   calc(clamp(var(--s-6), 7vh, var(--s-10)) + var(--hero-cue-space));
  }
}

/* ===== 4. Section scaffolding ===== */

/* Hash navigation must never land a heading beneath the fixed header. */
.section[id],
.hero[id] {
  scroll-margin-top: calc(73px + var(--s-4));
}

.split {
  display: grid;
  gap: var(--s-5);
  margin-bottom: var(--s-8);
}
@media (min-width: 900px) {
  .split {
    grid-template-columns: 1.1fr 1fr;
    align-items: end;
    gap: var(--s-8);
  }
}

/* ===== 5. Grid + cards ===== */

.grid {
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
}
.grid--4 {
  grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 899px) {
  .grid--4 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 639px) {
  .grid--4 { grid-template-columns: 1fr; }
}

.prob {
  margin-top: var(--s-2);
}

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--surface);
  padding: var(--s-6) var(--s-5);
  overflow: hidden;
  transition: background-color var(--d-2) var(--ease-out);
}
.card:hover { background: var(--surface-2); }

.card__metric {
  display: block;
  /* Was --s-5, which read as detached from the card it labels. */
  margin-bottom: var(--s-4);
  color: var(--ink-3);
}
.card__t {
  font-size: var(--t-body);
  font-weight: 600;
  color: var(--ink);
  margin-bottom: var(--s-2);
  /* Titles wrap to one or two lines; reserving two keeps every card's body
     copy starting on the same line across the row. */
  min-height: calc(var(--t-body) * 2 * 1.3);
}
.card__b {
  font-size: var(--t-small);
}

.card__edge {
  position: absolute;
  left: 0; top: 0;
  width: 0;
  height: 2px;
  background: var(--signal);
  transition: width var(--d-3) var(--ease-out);
}
.card:hover .card__edge,
.card:focus-within .card__edge {
  width: 100%;
}

.note {
  position: relative;
  margin-top: var(--s-7);
  padding-left: var(--s-6);
  max-width: var(--measure);
}
.note__bar {
  position: absolute;
  left: 0; top: 0.2em; bottom: 0.2em;
  width: 1px;
  background: var(--signal-dim);
}
.note p {
  color: var(--ink-2);
  font-size: var(--t-small);
}

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

/* ===== 6. Lattice / pillars ===== */

.lattice {
  display: grid;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
@media (min-width: 900px) {
  .lattice {
    grid-template-columns: 1fr 1fr;
  }
  .pillar:nth-child(odd) { border-right: 1px solid var(--line); }
}

.pillar {
  padding: var(--s-6) 0;
  border-top: 1px solid var(--line);
}
.pillar:first-child,
.lattice > .pillar:nth-child(-n+2):first-child {
  border-top: 0;
}
@media (min-width: 900px) {
  .pillar { padding: var(--s-6) var(--s-6); }
  .pillar:nth-child(1),
  .pillar:nth-child(2) { border-top: 0; }
}

.pillar__hd {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  width: 100%;
  background: none;
  border: none;
  /* Vertical padding brings the control to a comfortable touch target
     (was 28px, below the 44px guidance). Negative inline margin keeps the
     text optically flush with the rest of the column. */
  padding: var(--s-3) 0;
  margin-bottom: var(--s-2);
  text-align: left;
  cursor: pointer;
  color: inherit;
  min-height: 44px;
}
.pillar__n {
  color: var(--ink-4);
  font-size: var(--t-micro);
  flex: none;
}
.pillar__t {
  flex: 1;
  font-size: var(--t-lead);
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  letter-spacing: var(--display-tracking);
  color: var(--ink);
}
.pillar__ind {
  flex: none;
  position: relative;
  width: 16px;
  height: 16px;
}
.pillar__ind::before,
.pillar__ind::after {
  content: "";
  position: absolute;
  background: var(--ring-dim);
  transition: transform var(--d-2) var(--ease-out), background-color var(--d-2) var(--ease-out);
}
.pillar__ind::before {
  left: 0; top: 50%;
  width: 100%; height: 1px;
  transform: translateY(-50%);
}
.pillar__ind::after {
  left: 50%; top: 0;
  width: 1px; height: 100%;
  transform: translateX(-50%);
}
.pillar__hd[aria-expanded="true"] .pillar__ind::after {
  transform: translateX(-50%) rotate(90deg) scaleY(0);
}
.pillar__hd:hover .pillar__ind::before,
.pillar__hd:hover .pillar__ind::after {
  background: var(--signal);
}

.pillar__s {
  color: var(--ink-3);
  font-size: var(--t-small);
  max-width: 46ch;
  margin-bottom: var(--s-1);
}

.pillar__d {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--d-3) var(--ease-out);
}
.pillar__d > * {
  overflow: hidden;
}
.pillar.is-open .pillar__d {
  grid-template-rows: 1fr;
}

.pillar__list {
  padding-top: var(--s-4);
}
.pillar__list li {
  position: relative;
  padding-left: var(--s-5);
  color: var(--ink-2);
  font-size: var(--t-small);
  line-height: var(--lead-snug);
}
.pillar__list li + li { margin-top: var(--s-3); }
.pillar__list li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.65em;
  width: var(--s-3);
  height: 1px;
  background: var(--ring-dim);
}

.practice__note {
  margin-top: var(--s-6);
  color: var(--ink-4);
}

@media (prefers-reduced-motion: reduce) {
  .pillar__d { transition: none; }
  .pillar__ind::before,
  .pillar__ind::after { transition: none; }
}

/* ===== 7. Pipeline ===== */

.section--pipe .digest {
  margin-top: var(--s-8);
}

.pipe {
  margin-top: var(--s-2);
}

.pipe__rail {
  position: relative;
  display: flex;
  gap: var(--s-7);
  overflow-x: auto;
  padding-bottom: var(--s-5);
  margin-bottom: var(--s-6);
  border-bottom: 1px solid var(--line);
  scrollbar-width: none;
  /* Connector line. Painted as a background rather than an absolutely
     positioned ::before because an abspos child is sized against the
     PADDING BOX (the visible 350px window), not the 632px scroll content —
     so the line stopped just before "04 Weigh" and everything scrolled into
     view after it was unconnected. `background-attachment: local` anchors
     the background to the scrolled content, so 100% means the full
     scrollWidth. Verified by pixel-sampling the scrolled-right state. */
  background-image: linear-gradient(var(--line-2), var(--line-2));
  background-repeat: no-repeat;
  background-size: 100% 1px;
  background-position: 0 9px;
  background-attachment: local;
  /* Snap so a swiped rail always settles on a stage rather than mid-tab. */
  scroll-snap-type: x proximity;
  /* The rail overflows at ~390px (632px of tabs in 350px). Without an edge
     affordance the last two stages are simply invisible and the viewer never
     learns they exist. The mask fades the cut edge to imply more content. */
  -webkit-mask-image: linear-gradient(90deg, rgb(0 0 0) 0, rgb(0 0 0) 93%, rgba(0 0 0 / 0.55) 100%);
  mask-image: linear-gradient(90deg, rgb(0 0 0) 0, rgb(0 0 0) 93%, rgba(0 0 0 / 0.55) 100%);
}
/* Roomier top padding, but ONLY from 900px up. This lived in the 640px
   block and, because it comes later in the cascade than the narrow rules,
   it silently removed the art band for 640-899px — large phones and small
   tablets, the exact range least likely to get checked. */
@media (min-width: 900px) {
  .hero { --hero-in-pad-top: clamp(var(--s-7), 8vh, var(--s-10)); }
}

@media (min-width: 900px) {
  /* Everything fits on wide viewports; no fade, no snap. */
  .pipe__rail {
    -webkit-mask-image: none;
    mask-image: none;
    scroll-snap-type: none;
  }
}
.pipe__tab { scroll-snap-align: start; }
.pipe__rail::-webkit-scrollbar { display: none; }

.pipe__tab {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex: none;
  background: none;
  border: none;
  /* Touch target: bare buttons measured 23px tall. Vertical padding lifts
     them to 44px without changing the rail's visual rhythm. */
  padding: var(--s-3) 0;
  min-height: 44px;
  cursor: pointer;
  color: var(--ink-3);
  transition: color var(--d-2) var(--ease-out);
}
.pipe__tab:hover { color: var(--ink-2); }

.pipe__dot {
  width: 9px;
  height: 9px;
  border-radius: var(--r-full);
  background: var(--surface-2);
  border: 1px solid var(--ring-dim);
  flex: none;
  transition: background-color var(--d-2) var(--ease-out), border-color var(--d-2) var(--ease-out);
}
.pipe__tab.is-on .pipe__dot {
  background: var(--signal);
  border-color: var(--signal);
}

.pipe__tab .pipe__n {
  color: var(--ink-4);
  font-size: var(--t-nano);
}
.pipe__tab .pipe__t {
  font-size: var(--t-small);
  white-space: nowrap;
}
.pipe__tab.is-on { color: var(--signal-2); }
.pipe__tab.is-on .pipe__t { color: var(--signal-2); }

.pipe__stage {
  display: grid;
  /* Was --s-8, which compounded with the rail's own bottom margin and left
     the loosest vertical gap on the page. */
  gap: var(--s-6);
}
@media (min-width: 900px) {
  .pipe__stage {
    grid-template-columns: 1.1fr 1fr;
    align-items: center;
  }
}

.pipe__panel[hidden] { display: none; }

.pipe__count {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  margin-bottom: var(--s-5);
}
.pipe__num {
  font-size: clamp(3rem, 2rem + 4vw, 5rem);
  color: var(--ink);
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.pipe__unit { color: var(--ink-4); }

.pipe__body {
  max-width: 46ch;
  margin-bottom: var(--s-5);
}

.pipe__tag {
  display: inline-block;
  padding: var(--s-1) var(--s-3);
  border: 1px solid var(--line-2);
  border-radius: var(--r-full);
  color: var(--ink-3);
}

.pipe__viz {
  position: relative;
  height: 200px;
  border: 1px solid var(--line);
  background: var(--surface);
  border-radius: var(--r-2);
  overflow: hidden;
}
.pipe__viz canvas {
  width: 100%;
  height: 100%;
}
.pipe__vizlabel {
  position: absolute;
  right: var(--s-4);
  bottom: var(--s-3);
  color: var(--ink-4);
  pointer-events: none;
}

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

/* ===== 8. Digest ===== */

.digest {
  margin: var(--s-9) 0 0;
  border: 1px solid var(--line);
  background: var(--surface);
  border-radius: var(--r-2);
  overflow: hidden;
}

.digest__hd {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-5) var(--s-6);
  border-bottom: 1px solid var(--line);
}
.digest__t {
  font-family: var(--font-display);
  font-size: var(--t-lead);
  color: var(--ink);
}

.digest__body {
  display: grid;
  gap: 1px;
  background: var(--line);
}
@media (min-width: 640px) {
  .digest__body { grid-template-columns: repeat(3, 1fr); }
  /* "Set aside" is a footer strip, not a fourth column: as a peer it left
     two empty grid cells, which read as missing content. */
  .dblock--dim { grid-column: 1 / -1; }
}

.dblock {
  background: var(--surface);
  padding: var(--s-5) var(--s-6);
}
.dblock__l { margin-bottom: var(--s-4); }
.dblock li {
  position: relative;
  padding-left: var(--s-5);
  font-family: var(--font-data);
  font-size: var(--t-micro);
  line-height: var(--lead-snug);
  color: var(--ink-2);
}
.dblock li::before {
  content: "—";
  position: absolute;
  left: 0;
  color: var(--ink-4);
}
.dblock li + li { margin-top: var(--s-3); }

.dblock--signal {
  border-left: 2px solid var(--signal);
  padding-left: calc(var(--s-6) - 2px);
}
.dblock--signal .dblock__l { color: var(--signal-2); }
.dblock--signal li { color: var(--ink); }
.dblock--signal li::before { color: var(--signal-dim); }

.dblock--plain li { color: var(--ink-2); }

.dblock--dim {
  color: var(--ink-4);
  display: flex;
  align-items: baseline;
  gap: var(--s-5);
  padding-block: var(--s-4);
}
.dblock--dim .dblock__l { margin-bottom: 0; flex: none; }
.dblock--dim li {
  /* This line is the punchline of the whole digest (176 items filtered out);
     --ink-4 rendered it effectively invisible. */
  color: var(--ink-3);
  font-size: var(--t-micro);
}
.dblock--dim ul { margin: 0; }

/* ===== 9. Chips ===== */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-bottom: var(--s-7);
}

.chip {
  display: inline-flex;
  align-items: center;
  /* Fixed height + optical padding: pills of differing word length were
     reading as inconsistently weighted blobs rather than one set. */
  min-height: 30px;
  padding: 0 var(--s-4);
  border: 1px solid var(--line-3);
  border-radius: var(--r-full);
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  color: var(--ink-2);
  background: var(--a-04);
  white-space: nowrap;
}

.chip--alt {
  font-family: var(--font-data);
  color: var(--ink-3);
  background: transparent;
}

/* ===== 10. Qual ===== */

.qual {
  display: grid;
  gap: var(--s-7);
  margin-top: var(--s-2);
}
@media (min-width: 640px) {
  .qual { grid-template-columns: 1fr 1fr; gap: var(--s-9); }
}

/* The two columns must differ STRUCTURALLY, not only by accent colour:
   mirror-symmetric columns distinguished by hue alone cannot be read
   pre-cognitively, and the "poor fit" grey fell below comfortable contrast. */
.qual__col {
  border-top: 1px solid var(--line);
  /* Both columns carry identical padding so labels and first bullets share
     a baseline; only the treatment differs, never the box metrics. */
  padding: var(--s-5) var(--s-5) var(--s-5);
}
.qual__l { margin-bottom: var(--s-4); }

.qual__col li {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  font-size: var(--t-small);
  color: var(--ink-2);
}
.qual__col li + li { margin-top: var(--s-3); }

/* Marker is a glyph drawn in CSS, so the two sides differ in SHAPE. */
.qual__gl {
  position: relative;
  flex: none;
  width: 11px;
  height: 11px;
  margin-top: 0.34em;
}
.qual__gl::before,
.qual__gl::after {
  content: "";
  position: absolute;
  background: currentColor;
}

/* Strong fit: an affirmative rising tick, amber, on a raised panel. */
.qual__col--good {
  border-top-color: var(--signal-dim);
  /* Contained panel: a tint that merely fades leaves the box's bottom
     ambiguous. A resolved bottom edge makes it read as intentional. */
  background: linear-gradient(180deg, var(--signal-a-08), transparent 72%);
  border-bottom: 1px solid color-mix(in srgb, var(--signal-dim) 45%, transparent);
  margin-left: calc(var(--s-5) * -1);
}
.qual__col--good .qual__l { color: var(--signal-2); }
.qual__col--good li { color: var(--ink); }
.qual__col--good .qual__gl { color: var(--signal); }
.qual__col--good .qual__gl::before {
  width: 4px; height: 1.5px;
  left: 0; top: 6px;
  transform: rotate(45deg);
  transform-origin: left center;
}
.qual__col--good .qual__gl::after {
  width: 9px; height: 1.5px;
  left: 2.6px; top: 6px;
  transform: rotate(-45deg);
  transform-origin: left center;
}

/* Poor fit: a subtractive horizontal bar, cool and recessive. */
.qual__col--poor .qual__l { color: var(--ink-3); }
.qual__col--poor li { color: var(--ink-3); }
.qual__col--poor .qual__gl { color: var(--ring-dim); }
.qual__col--poor .qual__gl::before {
  width: 9px; height: 2px;
  left: 1px; top: 4.5px;
  opacity: 0.9;
}

/* ===== 11. Steps ===== */

.steps {
  display: grid;
  gap: var(--s-7);
  margin-bottom: var(--s-8);
}
@media (min-width: 640px) {
  .steps { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1100px) {
  .steps {
    grid-template-columns: repeat(4, 1fr);
    gap: 0;
  }
  .step {
    position: relative;
    padding-right: var(--s-6);
  }
  .step + .step { padding-left: var(--s-6); }
  .step + .step::before {
    content: "";
    position: absolute;
    left: 0; top: 0.6em;
    width: 1px;
    height: calc(100% - 1.2em);
    background: var(--line-2);
  }
}

.step__n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.4rem;
  height: 2.4rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r-full);
  color: var(--ink-3);
  font-size: var(--t-small);
  margin-bottom: var(--s-4);
}
.step__t {
  font-size: var(--t-body);
  font-weight: 600;
  color: var(--ink);
  margin-bottom: var(--s-2);
}
.step__b { font-size: var(--t-small); }

/* ===== 12. Ex + CTA ===== */

.ex {
  padding-top: var(--s-7);
  border-top: 1px solid var(--line);
  margin-bottom: var(--s-8);
}
.ex .label { margin-bottom: var(--s-4); }
/* Constrain the row so six chips break 3+3 rather than leaving a single
   orphaned chip alone on a second line. */
.ex .chips { margin-bottom: 0; max-width: 46rem; }

.cta {
  position: relative;
  border: 1px solid var(--line-2);
  border-top: 1px solid var(--signal-dim);
  border-radius: var(--r-2);
  padding: var(--s-7) var(--s-6);
  overflow: hidden;
  background:
    linear-gradient(180deg, var(--surface-2), var(--surface));
}
/* Corner register marks: the instrument motif, and they resolve the panel's
   edges so it reads as a deliberate object rather than a floating rect. */
.cta::before,
.cta::after {
  content: "";
  position: absolute;
  width: 10px;
  height: 10px;
  border-color: var(--signal-dim);
  border-style: solid;
  border-width: 0;
  pointer-events: none;
}
.cta::before {
  top: var(--s-3); left: var(--s-3);
  border-top-width: 1px; border-left-width: 1px;
}
.cta::after {
  bottom: var(--s-3); right: var(--s-3);
  border-bottom-width: 1px; border-right-width: 1px;
}

.cta__glow {
  position: absolute;
  top: -30%;
  right: -10%;
  width: 55%;
  aspect-ratio: 1;
  /* Committed, and off-axis: a timid centred glow read as an accident. */
  background: radial-gradient(closest-side, var(--signal-a-08), transparent 70%);
  pointer-events: none;
}

.cta__in {
  position: relative;
  z-index: 1;
  max-width: 48rem;
}
.cta__in h3 { margin-bottom: var(--s-4); }
.cta__in p { margin-bottom: var(--s-6); }

.cta__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-6);
}

/* Keeps "or email" glued to the address as one phrase, so the row's 32px
   flex gap separates the BUTTON from the phrase rather than splitting the
   phrase itself. */
.cta__alt {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4em;
  flex-wrap: wrap;
}

.cta__mail {
  position: relative;
  color: var(--ink);
  padding-bottom: 2px;
}
/* The "or email" lead-in is real copy and lives in content.js as a sibling
   <span>, NOT in a ::before here. A generated-content string cannot hold a
   reliable trailing space (it is collapsed at the inline box edge, which is
   exactly how "or emailhello@ringplane.ai" shipped), it is invisible to the
   copy review, and it is unselectable. */
.cta__lead {
  color: var(--ink-3);
}
.cta__mail::after {
  content: "";
  position: absolute;
  left: 0; right: 100%; bottom: 0;
  height: 1px;
  background: var(--signal);
  transition: right var(--d-2) var(--ease-out);
}
.cta__mail:hover { color: var(--ink); }
.cta__mail:hover::after { right: 0; }

@media (min-width: 900px) {
  .cta { padding: var(--s-8) var(--s-8); }
}

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

/* ===== 13. Footer ===== */

.ftr {
  border-top: 1px solid var(--line);
  padding-block: var(--s-7);
}

.ftr__in {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
}

.ftr__brand {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}
.ftr__brand .mark {
  width: 26px;
  height: 26px;
  color: var(--ring-dim);
}
.ftr__name {
  font-family: var(--font-data);
  font-size: var(--t-small);
  color: var(--ink-2);
}
.ftr__blurb {
  color: var(--ink-4);
  font-size: var(--t-nano);
}

.ftr__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-5);
}
.ftr__links a {
  font-size: var(--t-micro);
  color: var(--ink-3);
  transition: color var(--d-2) var(--ease-out);
}
.ftr__links a:hover { color: var(--ink); }

.ftr__c {
  color: var(--ink-4);
  font-size: var(--t-nano);
}

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

/* ===== 14. Command palette ===== */

.pal[hidden] { display: none !important; }

.pal {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: min(18vh, var(--s-10));
}

.pal__scrim {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  animation: pal-fade var(--d-2) var(--ease-out);
}

.pal__box {
  position: relative;
  z-index: 1;
  width: min(560px, calc(100% - var(--s-6) * 2));
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-3);
  box-shadow: var(--shadow-3);
  overflow: hidden;
  animation: pal-in var(--d-3) var(--ease-spring);
}

@keyframes pal-fade {
  from { opacity: 0; }
}
@keyframes pal-in {
  from { opacity: 0; transform: translateY(-10px) scale(0.98); }
}

.pal__hd {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--line);
}

.pal__list {
  max-height: 46vh;
  overflow-y: auto;
  padding: var(--s-2);
}

.pal__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-2);
  cursor: pointer;
  color: var(--ink-2);
  transition: background-color var(--d-1) var(--ease-out), color var(--d-1) var(--ease-out);
}
.pal__item:hover,
.pal__item.is-sel {
  background: var(--signal-a-08);
  color: var(--ink);
}
.pal__item.is-sel { outline: 1px solid var(--signal-dim); outline-offset: -1px; }
.pal__item .mono { color: var(--ink-4); }

.pal__ft {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  border-top: 1px solid var(--line);
  color: var(--ink-4);
}

.pal__themes {
  display: flex;
  gap: var(--s-2);
}

.pal__chip {
  padding: var(--s-1) var(--s-3);
  border: 1px solid var(--line-2);
  border-radius: var(--r-full);
  background: transparent;
  color: var(--ink-3);
  font-family: var(--font-data);
  font-size: var(--t-nano);
  cursor: pointer;
  transition: color var(--d-2) var(--ease-out), border-color var(--d-2) var(--ease-out);
}
.pal__chip:hover { color: var(--ink-2); }
.pal__chip.is-on,
.pal__chip[aria-pressed="true"] {
  color: var(--signal-2);
  border-color: var(--signal-dim);
  background: var(--signal-a-08);
}

@media (prefers-reduced-motion: reduce) {
  .pal__scrim, .pal__box { animation: none; }
  .pal__item { transition: none; }
  .pal__chip { transition: none; }
}

/* ===== Runtime state classes =====================================
   Added by JavaScript at execution time; no static markup carries them.
   ================================================================== */

/* A pipeline stage already traversed: the rail reads as a completed path
   rather than a set of unrelated buttons. */
.pipe__tab.is-past .pipe__dot {
  background: var(--signal-dim);
  border-color: var(--signal-dim);
}
.pipe__tab.is-past .pipe__n { color: var(--ink-3); }
.pipe__tab.is-past .pipe__t { color: var(--ink-2); }

/* Hero telemetry reaching full alignment — the one moment the readout
   earns amber. */
.readout__v.is-resolved {
  color: var(--signal);
  transition: color var(--d-3) var(--ease-out);
}

/* Set once the runtime has booted. Lets purely-decorative affordances stay
   hidden until something can actually drive them. */
.js-ready .hero__scroll { opacity: 1; }
.hero__scroll { opacity: 0; transition: opacity var(--d-3) var(--ease-out); }

@media (prefers-reduced-motion: reduce) {
  .readout__v.is-resolved,
  .hero__scroll { transition: none; }
}

/* ===== Touch targets ==============================================
   Inline text links are only as tall as their line box (~15-21px).
   On touch pointers, pad them to a comfortable target without
   disturbing desktop layout or the inline flow.
   ================================================================== */
/* WCAG 2.5.8 (AA) sets a 24px minimum target on ANY pointer, not just touch.
   Standalone link lists must satisfy it; inline links in prose are exempt. */
.ftr__links a,
.cta__mail {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}

@media (pointer: coarse) {
  /* WCAG 2.5.5 / Apple HIG: 44px minimum on touch. The small header buttons
     and the brand lockup were 37-40px. Padding is grown rather than font
     size, so the visual weight of the header is unchanged. */
  .btn--sm,
  .hdr__brand {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  .ftr__links a,
  .cta__mail,
  .hdr__nav a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  .ftr__links { gap: var(--s-2) var(--s-5); }
}

/* ===== Pipeline rail affordance =====================================
   The stage rail overflows on narrow viewports; this hint appears only
   where scrolling is actually required.
   ==================================================================== */
.pipe__hint {
  margin: calc(var(--s-6) * -1) 0 var(--s-6);
  color: var(--ink-4);
  font-size: var(--t-nano);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}
@media (min-width: 900px) {
  .pipe__hint { display: none; }
}
