/* ─────────────────────────────────────────────────────────────────────────
   page-services.css — the two index pages: /services/ and /pricing/.

   Both pages are RULED SHEETS at page scale. There is not a card, a shadow,
   a gradient or a filled badge in this file; regions are separated by 1px
   --graphite rules, columns are separated by gap and one hairline, and white
   dominates by a wide margin. Green appears on exactly three things here —
   a coverage tick, a filled module cell and a drawn confirmation tick —
   every one of them a state that is true rather than a decoration, and each
   of them a few square pixels.

   Everything is prefixed `sv-` and scoped so that nothing in this file can
   reach the home page, a service page, or any component another file owns.
   It EXTENDS page.css rather than replacing it: `.page-main`, `.page-head`,
   `.page-section`, `.page-lede`, `.rule-list`, `.cta` and `.ghost-link` are
   page.css's and are used unchanged. Every value is a var() — there is not a
   raw hex in this file.

   ── LOAD ORDER ────────────────────────────────────────────────────────────

   Both shells need, in <head>, in this order:

     <link rel="stylesheet" href="/src/design/tokens.css">
     <link rel="stylesheet" href="/src/design/base.css">
     <link rel="stylesheet" href="/src/design/page.css">
     <link rel="stylesheet" href="/src/design/dashboard.css">
     <link rel="stylesheet" href="/src/design/page-services.css">

   and, at the foot of <body>:

     /services/  <script type="module" src="/src/pages/services-index.js"></script>
     /pricing/   <script type="module" src="/src/pages/pricing.js"></script>

   `hero.css`, `sections.css` and `flow.css` are not used by either page.

   ── THE REVEAL ENGINE ─────────────────────────────────────────────────────

   The director writes ONE number per section — `--sv-in`, 0 → 1 — and every
   reveal on both pages is derived from it here. No JavaScript writes an
   opacity, a transform or a dash offset on these pages; it writes one custom
   property and the cascade does the rest.

   The mechanism is a per-element window into that one number:

       --sv-at    when this element starts, in units of --sv-in
       --sv-dur   how long it takes, in the same units
       --sv-p     the element's own eased-in 0 → 1

   which makes a stagger a matter of setting `--sv-at` from an index, and
   costs one style write per section per frame instead of one per element.

   THE ONE ARITHMETIC RULE: `--sv-at + --sv-dur` must never exceed 1 for any
   element, or that element never finishes — including in the default state
   below, where `--sv-in` is 1 and nothing is driving it. Every stagger in
   this file is chosen against that ceiling and the worst case is noted where
   it is close.

   --sv-in DEFAULTS TO 1, so a page whose director never starts — reduced
   motion, or a module that failed to load — arrives fully composed rather
   than invisible. Nothing here is the only thing making content visible.
   ───────────────────────────────────────────────────────────────────────── */

[data-sv-section] { --sv-in: 1; }

.sv-rise {
  --sv-at: 0;
  --sv-dur: 0.4;
  --sv-p: clamp(0, calc((var(--sv-in, 1) - var(--sv-at)) / var(--sv-dur)), 1);
  opacity: var(--sv-p);
  /* Transform and opacity only, and only ever these two. */
  transform: translate3d(0, calc((1 - var(--sv-p)) * 8px), 0);
}

/* A <tr> may be transformed, but it becomes a containing block for its cells
   while it is, which is a layout risk for one pixel of rise. Table rows
   fade; they do not travel. */
tr.sv-rise { transform: none; }

/* The site's one visually-hidden idiom, used for text that must exist for a
   screen reader at one width and be visible at another. */
.sv-vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ══ /services/ ═══════════════════════════════════════════════════════════ */

/* ── the live index ────────────────────────────────────────────────────── */

.sv-live { list-style: none; margin: 0; padding: 0; }

/* THE ROW IS THE HIT TARGET AND THE LINK IS THE NAME. `position: relative`
   is what the stretched overlay on `.sv-row-link::after` resolves against —
   see the note there. */
.sv-row {
  --sv-at: calc(var(--i, 0) * 0.05);
  --sv-dur: 0.4;
  --sv-p: clamp(0, calc((var(--sv-in, 1) - var(--sv-at)) / var(--sv-dur)), 1);

  position: relative;
  display: grid;
  /* Type left, the moving element right — design law 4, in one declaration. */
  grid-template-columns: minmax(0, 24rem) minmax(0, 1fr);
  gap: var(--s5);
  align-items: start;
  padding: var(--s5) 0;
}

/* The row's rule DRAWS left to right as the row arrives. It is a
   pseudo-element rather than the row's own border so that the scaleX has
   something to act on that is not also carrying the row's layout. */
.sv-row::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 0;
  border-top: var(--hairline);
  transform: scaleX(var(--sv-p, 1));
  transform-origin: left;
  transition: border-color 240ms var(--ease-out);
}

/* The list closes with a rule of its own, so nine rows read as a block. */
.sv-row:last-child::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 0;
  border-top: var(--hairline);
  transition: border-color 240ms var(--ease-out);
}

.sv-index .sv-row-link {
  --sv-at: calc(var(--i, 0) * 0.05);
  --sv-dur: 0.4;
  --sv-p: clamp(0, calc((var(--sv-in, 1) - var(--sv-at)) / var(--sv-dur)), 1);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'bay  go'
    'name name'
    'line line'
    'hand hand';
  column-gap: var(--s3);
  row-gap: var(--s2);
  align-items: baseline;
}

/* THE COPY COLUMN RISES ON ITS OWN WINDOW, 60ms AHEAD OF ITS SHEET (0.06 in
   --sv-in units, against the house 600ms reveal). The rise is on the SPANS
   and never on the anchor itself: a transform on the anchor would make it
   the containing block for the stretched `::after` overlay below, and the
   full-row hit target would silently shrink to the type column — a settled
   translate3d(0,0,0) is still a transform, so the trap never releases.
   The go arrow is excluded because its transform belongs to the hover; it
   fades on the same window instead. Ceiling: 8 × 0.05 + 0.4 = 0.80. */
.sv-index .sv-row-link > span:not(.sv-row-go) {
  opacity: var(--sv-p);
  transform: translate3d(0, calc((1 - var(--sv-p)) * 8px), 0);
}

.sv-index .sv-row-link > .sv-row-go { opacity: var(--sv-p); }

/* THE WHOLE ROW IS CLICKABLE, AND THE LINK'S NAME IS STILL SHORT. The sheet
   beside the type is a SIBLING of this anchor, not a child: inside it, forty
   fragments of dashboard would be folded into the link's accessible name.
   This overlay is the other half of that decision — without it a 1500px row
   would have a 400px target. It is positioned and the sheet is not, so it
   paints above the sheet without a z-index anywhere. */
.sv-row-link::after {
  content: '';
  position: absolute;
  inset: 0;
}

/* Ink, at 3px, on the whole row — never the browser's own ring on the type
   column alone. */
.sv-row-link:focus-visible { outline: none; }

.sv-row-link:focus-visible::after {
  outline: 1px solid var(--ink);
  outline-offset: 3px;
}

.sv-row-bay {
  grid-area: bay;
  /* --ink-60 AT REST, NOT --leaf-ink. Nine inert bay numbers in green was
     the old page's outright break of design law 1: green means current is
     flowing, and a number is not flowing. Under a cursor or focus the row
     IS live — an active state, which is exactly what green is for — so the
     number takes --leaf-ink then and only then (see the hover block). */
  color: var(--ink-60);
  font-size: 12px;
  letter-spacing: var(--eyebrow-tracking);
  transition: color 240ms var(--ease-out);
}

.sv-row-go {
  grid-area: go;
  color: var(--ink-60);
  transition: transform 240ms var(--ease-out), color 240ms var(--ease-out);
}

.sv-row-name {
  grid-area: name;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(28px, 3.2vw, 46px);
  letter-spacing: var(--display-tracking);
  line-height: var(--display-leading);
}

.sv-row-line {
  grid-area: line;
  color: var(--ink-60);
  font-size: clamp(15px, 1.15vw, 17px);
  max-width: 30ch;
}

.sv-row-handles {
  grid-area: hand;
  margin-top: var(--s2);
  font-size: 11px;
  letter-spacing: 0.16em;
  color: var(--ink-60);
}

/* WHAT CHANGES ON HOVER — AND THE SHEET'S SPEED IS NOT ON THE LIST. A scene
   is a claim about how fast the system is; accelerating it because a cursor
   arrived would make the claim a function of the cursor.

   The row frames itself in ink: its own rule darkens, and so does the rule
   BELOW it — which belongs to the next row (`+ .sv-row::before`), or to the
   list's closing rule on the ninth. The bay number takes --leaf-ink, the
   arrow travels 4px, the packet core brightens. All at 240ms --ease-out,
   and everything here answers :focus-within identically, so a keyboard
   visitor gets the same live row a cursor does. */
.sv-row:hover::before,
.sv-row:focus-within::before,
.sv-row:hover + .sv-row::before,
.sv-row:focus-within + .sv-row::before,
.sv-row:last-child:hover::after,
.sv-row:last-child:focus-within::after { border-top-color: var(--ink); }

.sv-row:hover .sv-row-bay,
.sv-row:focus-within .sv-row-bay { color: var(--leaf-ink); }

.sv-row:hover .sv-row-go,
.sv-row:focus-within .sv-row-go { transform: translate3d(4px, 0, 0); color: var(--ink); }

/* The packet's core brightens to --leaf-hot. Same construction as
   `.dash-packet`, same light-source reading, one stop hotter — it is the
   only property of the sheet this file touches. */
.sv-row:hover .dash-packet,
.sv-row:focus-within .dash-packet {
  background: radial-gradient(circle at 36% 30%, var(--leaf-hot), var(--leaf-hot) 45%, var(--leaf));
}

/* THE SHEET'S SLOT: A FLOOR, NOT A CEILING.
 *
 * The register reserves every sheet with `aspect-ratio`, which is right when
 * the composition is authored against the box. It is wrong here. A `strip`
 * composes as one row of tiles only while its own container query has twelve
 * columns to give it — below 1000px it stacks the figure over the
 * instrument, and below 780px it stacks again — so any fixed ratio that
 * looks right at one width CUTS THE COMPOSITION IN HALF at another, and
 * `.dash`'s `overflow: clip` means it does so silently.
 *
 * So the box is a FLOOR and the sheet may exceed it. `::before` is an empty
 * grid item carrying the floor; the frame is stacked in the same 1×1 cell.
 * The row's height is therefore max(floor, what the sheet actually needs):
 * the box exists before the module runs, which is the zero-CLS reservation
 * the register asks for, and nothing can ever be clipped out of it.
 */
.sv-index .sv-dash {
  /* One beat after the row's copy — 0.06 of --sv-in is 60ms of the house
     600ms reveal. Ceiling: 8 × 0.05 + 0.06 + 0.4 = 0.86. */
  --sv-at: calc(var(--i, 0) * 0.05 + 0.06);
  --sv-dur: 0.4;
  --sv-p: clamp(0, calc((var(--sv-in, 1) - var(--sv-at)) / var(--sv-dur)), 1);
  /* Opacity only: the sheet fades in ALREADY MID-CYCLE, because it is a pure
     function of the shared clock and was never at frame 0. A dashboard that
     boots as you scroll past it reads as a loading screen. */
  opacity: var(--sv-p);
  display: grid;
  /* `minmax(0, 1fr)`, NEVER the implicit `auto` column. An auto track is
     sized from its contents, and a sheet carrying an activity feed has a
     min-content width of about 1300px because `.dash-feed-title` is
     `white-space: nowrap` — so an auto track hands the sheet that width and
     the whole page grows a horizontal scrollbar. The `0` floor is what makes
     the track's size the row's, and the sheet's the track's. */
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
}

/* THE FLOOR IS A HEIGHT, NOT A RATIO, and that is the whole point of it.
 *
 * A ratio floor scales with the column, and the sheet's contents do not: a
 * chrome bar, a figure and four feed rows are about the same number of
 * pixels tall at 360px as at 1000px. So a ratio that is right at a laptop is
 * far too short on a phone (nine ragged rows) and far too tall at 1600px
 * (nine bands of white). A height floor is the same for all nine at any one
 * width, which is what makes the index read as one instrument repeated
 * rather than nine unrelated boxes.
 *
 * 520px is the tallest of the nine, measured at every breakpoint from 360px
 * to 1600px, so in practice the floor IS the height and all nine rows are
 * exactly the same depth at any width. One number, no breakpoints. */
.sv-index .sv-dash::before {
  content: '';
  grid-area: 1 / 1;
  min-height: 520px;
}

/* The frame is stacked in the same cell as the floor, and it stretches the
   sheet to fill it: a sheet left at its own content height would sit in the
   top of a taller box with the packet rail floating in the middle of it, and
   its body track would stay one row short of what the feed needs. */
.sv-index .sv-dash > .dash-frame {
  grid-area: 1 / 1;
  min-width: 0;
  display: grid;
  grid-template-rows: minmax(min-content, 1fr);
}

/* The sheet fills the cell and is free to be taller than it. Every rule in
   this file that reaches into the register is scoped to `.sv-index`, so no
   sheet anywhere else on the site changes by a pixel. */
.sv-index .sv-dash .dash[data-format='strip'] { aspect-ratio: auto; min-height: 0; }

/* COMPRESSION, IN THE REGISTER'S OWN DROP ORDER.
 *
 * An index row is a glance, not a sheet. Six of the nine compose their strip
 * as a figure over a seven-row activity feed, which at a laptop's column
 * width is a 740px-tall dashboard — taller than the row's type, taller than
 * the other three bays, and not compressed by any reading of the word.
 *
 * So the feed is compressed here exactly as `dashboard.css` §7 compresses it
 * when a sheet runs out of room, in the same order and with the same
 * mechanism: THE SUBLINES GO FIRST — they are the least missed text on a
 * feed row — the rows drop to the register's own 44px floor, and the list
 * shows its first four steps. Titles are never dropped, because titles are
 * the sheet's meaning.
 *
 * Nothing is lost to a screen reader: the sheet's `aria-label` is composed
 * from the flow's step titles and still names all seven, and the service
 * page behind the row carries the whole workflow. This is scoped to
 * `.sv-index`, so no other sheet on the site is compressed by it.
 */
.sv-index .dash-feed-sub { display: none; }
.sv-index .dash-feed-row { min-height: 44px; }
.sv-index .dash-feed-row:nth-child(n + 5) { display: none; }

/* ── coverage ──────────────────────────────────────────────────────────── */

.sv-matrix {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

/* 12rem holds AUTOMATION, the longest bay name, at 15px Archivo 800. Every
   pixel above that is one the fifteen mono column heads do not get. */
.sv-mtx-corner { width: 12rem; }

.sv-mtx-head {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-60);
  text-align: center;
  padding: 0 var(--s1) var(--s3);
}

.sv-matrix tbody tr { border-top: var(--hairline); }
.sv-matrix tbody tr:last-child { border-bottom: var(--hairline); }

/* The one legal vertical hairline on this table, exactly as `.dash-matrix`
   allows one between its row-header column and its data. */
.sv-mtx-bay {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 15px;
  letter-spacing: var(--display-tracking);
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
  padding: var(--s3) var(--s3) var(--s3) 0;
  border-right: var(--hairline);
}

.sv-mtx-bay .tech {
  font-size: 11px;
  letter-spacing: 0.16em;
  color: var(--ink-60);
}

.sv-matrix td { padding: var(--s3) var(--s1); text-align: center; }

.sv-matrix tbody tr { --sv-at: calc(var(--i, 0) * 0.05); }

/* The tick. Same 7px construction as `.dash-cell.is-on`, and green for the
   same reason: it is a state that is true, not a decoration. The ticks land
   COLUMN BY COLUMN and last of all — the rules draw, the names arrive, then
   the coverage fills in left to right.

   Worst case: 0.35 + 14 × 0.03 + 0.22 = 0.99. Under the ceiling by 0.01, and
   the fifteen columns are counted from the data — if a sixteenth handled
   thing is ever added, drop the 0.03 to 0.028. */
.sv-tick {
  --sv-p: clamp(0, calc((var(--sv-in, 1) - 0.35 - var(--c, 0) * 0.03) / 0.22), 1);
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--leaf);
  opacity: var(--sv-p);
  transform: scale(calc(0.6 + 0.4 * var(--sv-p)));
}

/* The column word travels INSIDE the cell as real text, hidden while the
   column head above it is legible. It is what labels the tick once the table
   collapses, and being real text it can be read by a screen reader,
   selected, and translated — none of which `content: attr()` can promise. */
.sv-matrix .sv-mtx-word {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── the wall's slot, on both pages ────────────────────────────────────── */

/* The wall's height is a function of its LANE COUNT, not of a ratio, so it
   carries no aspect-ratio to reserve with. This floor is the desktop wall's
   own arithmetic — 48px head + 9 × 56px lanes + foot + padding — and it is
   released the instant the wall mounts, so the reservation can never be
   left over as a band of white under a shorter sheet. */
.sv-board-host { min-height: 640px; }
.sv-board-host:has(.dash-wall) { min-height: 0; }

/* Below 720px the wall restructures: 44px head, 9 × 64px lanes. */
@media (max-width: 760px) {
  .sv-board-host { min-height: 700px; }
}

.sv-board .sv-late { --sv-at: 0.45; margin-top: var(--s4); }

/* The wall's reduced-motion stepper sits on its frame, outside `.dash`, so
   dashboard.css's `.dash .dash-step` does not reach it and it would
   otherwise render as a raw browser button. Same treatment, restated. */
.sv-board-host .dash-step {
  margin: var(--s3) 0 0;
  background: none;
  border: var(--hairline);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.10em;
  padding: 10px var(--s3);
  min-height: 44px;          /* the hit-target floor, at every width */
  min-width: 44px;
  cursor: pointer;
}

.sv-board-host .dash-step:hover { border-color: var(--leaf-ink); color: var(--leaf-ink); }
.sv-board-host .dash-step:focus-visible { outline: 1px solid var(--ink); outline-offset: 3px; }

/* Nothing could be fetched: the reserved boxes go with the sheets that were
   going to fill them. The sections keep their heads and their approved
   lines — a dashboard is evidence, never the argument. */
[data-sv-no-dash] .sv-dash,
[data-sv-no-dash] .sv-board-host { display: none; }

/* ── the end rail ──────────────────────────────────────────────────────── */

.sv-end-rail {
  display: flex;
  align-items: center;
  gap: var(--s4);
  flex-wrap: wrap;
}

/* The rail arrives with the rows' own gesture: CTA first, the onward link
   one beat later. Ceiling: 1 × 0.08 + 0.4 = 0.48.

   OPACITY ONLY, no rise — deliberately, twice over. page.css transitions
   the CTA's transform for its :active press, and a transitioned property
   must never also be driven per-frame by scroll or the two fight; and the
   dash columns above establish that a fade alone is this page's arrival
   for things that already carry their own motion. */
.sv-end-rail .sv-rise { --sv-at: calc(var(--i, 0) * 0.08); transform: none; }

/* The CTA's hover is the home hero pill's, inherited whole from page.css —
   the surface steps one stop darker, :active presses 1px — so nothing is
   restated here. The onward link borrows the pill's other gesture: its
   arrow travels, exactly as `.pill-arrow` does inside `.pill-solid`. */
.sv-end-rail .sv-go {
  display: inline-block;             /* an inline can't carry a transform */
  transition: transform 240ms var(--ease-out);
}

.sv-end-rail .ghost-link:hover .sv-go,
.sv-end-rail .ghost-link:focus-visible .sv-go { transform: translate3d(4px, 0, 0); }

/* The same ink ring the rows wear, so focus reads identically down the
   whole page. */
.sv-end-rail .cta:focus-visible,
.sv-end-rail .ghost-link:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 3px;
}

/* ══ /pricing/ ════════════════════════════════════════════════════════════ */

.sv-compare {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

.sv-rc-corner { width: 11rem; }

.sv-compare thead th {
  vertical-align: bottom;
  font-weight: 400;
  padding: 0 var(--s4) var(--s4);
  border-left: var(--hairline);
}

/* THE NINE-CELL GLYPH does the comparison work three pricing cards were
   pretending to do. The hairline outline is standing structure — every one
   of the nine cells is always drawn — and only the FILL arrives, so a cell
   is never missing, it is only empty. */
.sv-glyph {
  display: grid;
  grid-template-columns: repeat(9, 1fr);
  gap: 3px;
  max-width: 170px;
  margin-bottom: var(--s3);
}

.sv-glyph i {
  position: relative;
  display: block;
  height: 16px;
  border: 1px solid var(--graphite);
}

/* The fill reads left to right, column by column, so the eye takes the three
   shapes in as 1 → 3 → 9: the whole argument of the page in one gesture.
   Worst case: 0.15 × 2 + 0.02 × 8 + 0.30 = 0.76. */
.sv-glyph i.is-on::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--leaf);
  --sv-p: clamp(0, calc((var(--sv-in, 1) - 0.15 * var(--c, 0) - 0.02 * var(--k, 0)) / 0.3), 1);
  opacity: var(--sv-p);
}

.sv-rc-name {
  display: block;
  font-size: 12px;
  letter-spacing: 0.16em;
  color: var(--ink);
}

.sv-compare tbody tr {
  --sv-at: calc(var(--i, 0) * 0.05);
  border-top: var(--hairline);
}

.sv-compare tbody tr:last-child { border-bottom: var(--hairline); }

.sv-compare tbody th {
  font-weight: 400;
  font-size: 11px;
  letter-spacing: 0.16em;
  color: var(--ink-60);
  white-space: nowrap;
  vertical-align: top;
  padding: var(--s4) var(--s4) var(--s4) 0;
}

/* Each row carries its own segment of the three column rules, which is why
   revealing the rows top to bottom IS the column rules drawing top to
   bottom. One gesture, no second mechanism. */
.sv-compare td {
  vertical-align: top;
  padding: var(--s4);
  border-left: var(--hairline);
  font-size: clamp(15px, 1.1vw, 17px);
}

/* The honest cell. Graphite, not a greyed-out "not included": an absent line
   is absent, and the table is more credible for showing it. */
.sv-rc-none { color: var(--graphite); }

.sv-rc-price td { letter-spacing: 0.16em; }

.sv-compare tfoot td {
  border-left: 0;
  padding: var(--s4) 0 0;
  color: var(--ink-60);
  max-width: 62ch;
}

.sv-compare tfoot tr { --sv-at: calc(var(--i, 0) * 0.05); }

/* The shape name repeated inside every cell — hidden while the column head
   above it is visible, and the label for the value once the table collapses. */
.sv-compare .sv-rc-key {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── every build includes ──────────────────────────────────────────────── */

.sv-ticked li {
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr);
  gap: var(--s3);
  align-items: center;
}

.sv-ticked li { --sv-at: calc(var(--i, 0) * 0.07); }

.sv-tick-draw { display: grid; place-items: center; }
.sv-tick-draw svg { display: block; width: 20px; height: 20px; }

/* Drawn once, on reveal, never on a loop — the one place in this file a
   stroke-dashoffset is animated, and `pathLength="1"` is what makes it a
   number rather than a measurement. No circle behind it, no filled badge. */
.sv-tick-draw path {
  fill: none;
  stroke: var(--leaf);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--sv-p));
}

.sv-ticked li .sv-tick-draw {
  --sv-p: clamp(0, calc((var(--sv-in, 1) - 0.25 - var(--i, 0) * 0.07) / 0.3), 1);
}

.sv-end-cta { margin-top: var(--s4); --sv-at: 0.2; }

/* ══ breakpoints ══════════════════════════════════════════════════════════ */

/* ── the index rows stack ──────────────────────────────────────────────── */

@media (max-width: 900px) {
  .sv-row {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s4);
    padding: var(--s4) 0;
  }
  .sv-row-line { max-width: none; }
}

/* ── the coverage matrix collapses ─────────────────────────────────────── */

/* Measured: the fifteen mono column heads and the 12rem row-header column
   need 1171px of table to sit on one line each, so the matrix holds only
   above a 1280px viewport. Below that it becomes one ruled block per bay,
   listing
   only what that bay handles — which is the same data, read down instead of
   across, and is what the row's own `handles` tag already says.

   HONEST LIMITATION: `display: block` on table parts drops the grid
   semantics a screen reader would otherwise get. It is survivable here
   because every visible mark in the collapsed layout is self-labelling — the
   row keeps its `<th scope="row">` and each tick keeps the handled word
   beside it as real text — so nothing depends on the header association that
   was lost. */
@media (max-width: 1280px) {
  .sv-matrix,
  .sv-matrix tbody,
  .sv-matrix tr,
  .sv-matrix th,
  .sv-matrix td { display: block; }

  .sv-matrix thead { display: none; }

  .sv-matrix tbody tr { padding: var(--s4) 0; }

  .sv-mtx-bay {
    border-right: 0;
    padding: 0 0 var(--s2);
    white-space: normal;
  }

  .sv-matrix td {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    padding: 0 var(--s4) 0 0;
    text-align: left;
  }

  .sv-matrix td:not([data-on]) { display: none; }

  .sv-matrix .sv-mtx-word {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
    font-size: 11px;
    letter-spacing: 0.16em;
    color: var(--ink-60);
  }
}

/* ── the comparison collapses ──────────────────────────────────────────── */

/* Below 860px the table becomes one ruled block per ROW — MODULES, then
   SHAPE, then LIVE IN — each with its three labelled values beneath it.
   NEVER a horizontally scrolling table.

   Collapsing by row rather than by shape is deliberate: three stacked shape
   blocks would put 01, 03 and 09 on three different screens, and comparing
   them is the only reason this page has a table at all. The column heads and
   their glyphs stay, as a strip above the blocks. */
@media (max-width: 860px) {
  .sv-compare,
  .sv-compare thead,
  .sv-compare tbody,
  .sv-compare tfoot,
  .sv-compare tr,
  .sv-compare th,
  .sv-compare td { display: block; }

  .sv-compare thead tr {
    display: flex;
    gap: var(--s3);
    padding-bottom: var(--s3);
    border-bottom: 2px solid var(--ink);   /* the rule of record, once */
  }

  /* Qualified by `.sv-compare` so it outweighs `.sv-compare td` below —
     the corner is a <td>, and the shorter selector loses to it. */
  .sv-compare .sv-rc-corner { display: none; }

  .sv-compare thead th {
    flex: 1 1 0;
    min-width: 0;
    padding: 0;
    border-left: 0;
  }

  .sv-glyph { max-width: none; gap: 2px; }
  .sv-glyph i { height: 12px; }
  .sv-rc-name { font-size: 11px; letter-spacing: 0.12em; }

  .sv-compare tbody tr { padding: var(--s4) 0; }
  .sv-compare tbody th { padding: 0 0 var(--s3); white-space: normal; }

  .sv-compare td {
    display: grid;
    grid-template-columns: 9rem minmax(0, 1fr);
    gap: var(--s3);
    align-items: baseline;
    border-left: 0;
    padding: 0 0 var(--s2);
  }

  .sv-compare .sv-rc-key {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
    font-size: 11px;
    letter-spacing: 0.16em;
    color: var(--ink-60);
  }
}

@media (max-width: 520px) {
  .sv-compare td { grid-template-columns: minmax(0, 1fr); gap: 2px; padding-bottom: var(--s3); }
}

/* ══ reduced motion ═══════════════════════════════════════════════════════

   Composed, not frozen. Everything arrived, every rule drawn, every tick
   landed, every module cell filled. The sheets paint their own settled state
   from their own modules; all this block does is make sure nothing here is
   left mid-window if `--sv-in` is ever written by something else.
   ═══════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  .sv-rise,
  .sv-index .sv-dash,
  .sv-index .sv-row-link > span {
    opacity: 1;
    transform: none;
  }

  .sv-row::before { transform: none; }

  .sv-tick,
  .sv-glyph i.is-on::after {
    opacity: 1;
    transform: none;
  }

  .sv-tick-draw path { stroke-dashoffset: 0; }

  /* Hover still answers — a state change is information, not motion — but
     it lands as a cut rather than a slide, and the arrows stay put. */
  .sv-row::before,
  .sv-row:last-child::after,
  .sv-row-bay,
  .sv-row-go,
  .sv-end-rail .sv-go { transition: none; }

  .sv-row:hover .sv-row-go,
  .sv-row:focus-within .sv-row-go { transform: none; }

  .sv-end-rail .ghost-link:hover .sv-go,
  .sv-end-rail .ghost-link:focus-visible .sv-go { transform: none; }
}
