/* ---------------------------------------------------------------------------
   Previo — page components.

   Loaded after tokens.css and base.css. No colour literals: every value is a
   token. Zero radius and no fills throughout — the grammar is hairline rules,
   generous space, and one accent used sparingly.
   --------------------------------------------------------------------------- */

/* --- Hero ------------------------------------------------------------------ */

.hero {
  padding: var(--s-7) 0 var(--s-6);
}

.hero h1 {
  /* Wide enough that the authored <br> controls the break rather than the
     measure fighting it. */
  max-width: 24ch;
  margin-bottom: var(--s-4);
}

.hero .lede {
  max-width: 44ch;
  margin-bottom: var(--s-4);
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-bottom: var(--s-5);
}

.hero-trust {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mid);
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule);
}

/* --- Stat row -------------------------------------------------------------- */

.stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-5);
  padding-top: var(--s-4);
  border-top: 1px solid var(--rule);
}

.stat-value {
  font-family: var(--font-serif);
  font-size: 2.6rem;
  line-height: 1;
  color: var(--black);
}

/* A column in .stats. Kept narrow enough that four sit on one line at 1440. */
.stat {
  flex: 0 0 auto;
  min-width: 6rem;
}

.stat-label {
  font-size: var(--t-caption);
  color: var(--mid);
  max-width: 22ch;
  margin-top: var(--s-1);
}

/* --- The source register ---------------------------------------------------
   The anti-overspill device. Each product line carries its own register, and a
   source name may appear in that page's prose ONLY if it appears in this table
   on the same page. scripts/check_site.py enforces both halves. */

.register {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-caption);
  margin-bottom: var(--s-3);
}

.register th {
  text-align: left;
  font-weight: 500;
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--mid);
  padding: 0 var(--s-3) var(--s-2) 0;
  border-bottom: 1px solid var(--rule-strong);
  white-space: nowrap;
}

.register td {
  padding: var(--s-2) var(--s-3) var(--s-2) 0;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
  color: var(--dim);
}

.register td:first-child {
  color: var(--ink);
  font-weight: 500;
  white-space: nowrap;
  border-left: 2px solid transparent;
  padding-left: var(--s-2);
  transition: border-color var(--d-fast) var(--ease-out);
}

/* The register is the credibility device, so it behaves like a document you can
   run a finger down: the whole row takes the toned ground and the leading rule
   goes strong. No highlight colour, no zebra. */
.register tbody tr {
  transition: background-color var(--d-fast) var(--ease-out);
}

.register tbody tr:hover {
  background: var(--ivory);
}

.register tbody tr:hover td:first-child {
  border-left-color: var(--rule-strong);
}

/* Third column is the reach: how far back, how often. A measurement, so mono. */
.register td:nth-child(3) {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.register-scroll {
  overflow-x: auto;
}

/* --- The coverage statement ------------------------------------------------
   Sources reached, sources not reached. The house doctrine is that the gap is a
   finding, not a caveat, so it is set at the same weight as everything else. */

.coverage {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--s-4);
  padding: var(--s-4) 0;
  border-top: 1px solid var(--rule-strong);
  border-bottom: 1px solid var(--rule-strong);
}

.coverage-col h3 {
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--mid);
  margin-bottom: var(--s-2);
}

.coverage-col ul {
  list-style: none;
  padding: 0;
  margin: 0;
  font-size: var(--t-caption);
  color: var(--dim);
}

.coverage-col li {
  padding: var(--s-1) 0;
  border-bottom: 1px solid var(--rule);
}

/* --- Product-line tiles ---------------------------------------------------- */

.lines {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--s-4);
}

.line-tile {
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule-strong);
}

.line-tile h3 {
  margin-bottom: var(--s-2);
}

.line-tile p {
  font-size: var(--t-caption);
  color: var(--dim);
  margin-bottom: var(--s-2);
}

/* --- Numbered steps and the pipeline --------------------------------------- */

.steps {
  display: grid;
  gap: 0;
}

.step {
  display: grid;
  grid-template-columns: 3rem 1fr;
  gap: var(--s-3);
  padding: var(--s-3) 0;
  border-bottom: 1px solid var(--rule);
}

.step:first-child {
  border-top: 1px solid var(--rule);
}

.step-num {
  font-family: var(--font-mono);
  font-size: var(--t-caption);
  letter-spacing: 0.1em;
  color: var(--mid);
  line-height: 1.9;
  transition: color var(--d-base) var(--ease-out);
}

.step-title {
  font-weight: 500;
  color: var(--black);
  margin-bottom: var(--s-1);
}

.step-desc {
  font-size: var(--t-caption);
  color: var(--dim);
  margin: 0;
  max-width: 52ch;
}

/* --- Two-column split ------------------------------------------------------ */

.split {
  display: grid;
  grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
  gap: var(--s-5);
  align-items: start;
}

@media (max-width: 52rem) {
  .split {
    grid-template-columns: 1fr;
    gap: var(--s-4);
  }
}

/* --- Check / feature grid -------------------------------------------------- */

.grid-2 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--s-4) var(--s-5);
}

.grid-item {
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule);
}

.grid-item h3 {
  font-family: var(--font-sans);
  font-size: var(--t-body);
  font-weight: 500;
  letter-spacing: 0;
  margin-bottom: var(--s-2);
}

.grid-item p {
  font-size: var(--t-caption);
  color: var(--dim);
  margin: 0;
}

.source-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1) var(--s-2);
  margin-top: var(--s-2);
  font-size: var(--t-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mid);
}

.source-chips span::after {
  content: " ·";
}

.source-chips span:last-child::after {
  content: "";
}

/* --- Score bands -----------------------------------------------------------
   No traffic light. The bands are ordinal and read top to bottom; position
   carries the meaning, so it survives without colour and is colour-blind safe.
   The one accent marks the band that stops a process, not every band. */

.band {
  display: grid;
  grid-template-columns: 5.5rem 1fr;
  gap: var(--s-3);
  padding: var(--s-3) 0;
  border-bottom: 1px solid var(--rule);
}

.band:first-child {
  border-top: 1px solid var(--rule);
}

.band-range {
  font-family: var(--font-mono);
  font-size: var(--t-caption);
  letter-spacing: 0.06em;
  color: var(--black);
  line-height: 1.9;
}

.band-name {
  font-weight: 500;
  margin-bottom: var(--s-1);
}

.band-severe .band-name {
  color: var(--severe);
}

.band-desc {
  font-size: var(--t-caption);
  color: var(--dim);
  margin: 0;
}

/* --- Plans ----------------------------------------------------------------- */

.plans {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--s-5);
}

.plan {
  border-top: 1px solid var(--rule-strong);
  padding-top: var(--s-3);
}

.plan-name {
  font-family: var(--font-serif);
  font-size: var(--t-section);
  margin-bottom: var(--s-2);
}

.plan-desc {
  font-size: var(--t-caption);
  color: var(--dim);
}

.plan ul {
  list-style: none;
  padding: 0;
  font-size: var(--t-caption);
  color: var(--dim);
}

.plan li {
  padding: var(--s-1) 0;
  border-bottom: 1px solid var(--rule);
}

/* --- FAQ ------------------------------------------------------------------- */

.faq-item {
  padding: var(--s-3) 0;
  border-bottom: 1px solid var(--rule);
}

.faq-item:first-child {
  border-top: 1px solid var(--rule);
}

.faq-q {
  font-weight: 500;
  margin-bottom: var(--s-1);
}

.faq-a {
  font-size: var(--t-caption);
  color: var(--dim);
  margin: 0;
  max-width: 58ch;
}

/* --- Study cards ----------------------------------------------------------- */

.studies {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: var(--s-5);
}

.study-card {
  display: block;
  border-top: 1px solid var(--rule-strong);
  padding-top: var(--s-3);
  color: inherit;
  text-decoration: none;
}

.study-card:hover {
  text-decoration: none;
}

.study-card:hover .study-title {
  color: var(--teal);
}

.study-kicker {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--mid);
  margin-bottom: var(--s-2);
}

.study-figure {
  font-family: var(--font-serif);
  font-size: 2.2rem;
  line-height: 1;
  color: var(--black);
}

.study-figure-label {
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mid);
  margin-bottom: var(--s-3);
}

.study-title {
  font-family: var(--font-serif);
  font-size: var(--t-section);
  line-height: 1.2;
  margin-bottom: var(--s-2);
  transition: color 0.18s ease;
}

.study-lede {
  font-size: var(--t-caption);
  color: var(--dim);
}

/* --- Timeline -------------------------------------------------------------- */

.timeline-entry {
  display: grid;
  grid-template-columns: 9rem 1fr;
  gap: var(--s-3);
  padding: var(--s-4) 0;
  border-bottom: 1px solid var(--rule);
  break-inside: avoid;
}

.timeline-entry:first-child {
  border-top: 1px solid var(--rule);
}

.timeline-date {
  font-size: var(--t-caption);
  font-weight: 500;
  color: var(--mid);
  letter-spacing: 0.04em;
}

.timeline-body h3 {
  font-family: var(--font-sans);
  font-size: var(--t-body);
  font-weight: 500;
  letter-spacing: 0;
  margin-bottom: var(--s-2);
}

.timeline-body p {
  font-size: var(--t-caption);
  color: var(--dim);
  max-width: 58ch;
}

.timeline-source {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: 0.06em;
  color: var(--dim);
}

@media (max-width: 40rem) {
  .timeline-entry,
  .step,
  .band {
    grid-template-columns: 1fr;
    gap: var(--s-2);
  }
}

/* --- The record card -------------------------------------------------------
   What was in the public record, and when. Deliberately NOT a score card: no
   number, no band, no deduction column. */

.record {
  border: 1px solid var(--rule-strong);
  padding: var(--s-4);
  max-width: 44rem;
}

.record-label {
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--mid);
  margin-bottom: var(--s-3);
}

.record-subject {
  font-family: var(--font-serif);
  font-size: var(--t-section);
  margin-bottom: var(--s-1);
}

.record-context {
  font-size: var(--t-caption);
  color: var(--mid);
  margin-bottom: var(--s-3);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

.record-list {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--s-3);
  font-size: var(--t-caption);
}

.record-list li {
  display: flex;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-2) 0;
  border-bottom: 1px solid var(--rule);
}

.record-when {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--mid);
  white-space: nowrap;
}

/* The other half of a record row: what the record said. */
.record-what {
  flex: 1;
  color: var(--ink);
}

.record-note {
  font-size: var(--t-micro);
  line-height: 1.6;
  color: var(--mid);
  margin: 0;
}

/* --- Callout ---------------------------------------------------------------
   A single hairline-ruled aside. Used for the method statement and for the
   permanent illustrative-dataset marker on specimen pages, which may never be
   hidden or clipped. */

.callout {
  border-left: 2px solid var(--rule-strong);
  padding: var(--s-2) 0 var(--s-2) var(--s-3);
  font-size: var(--t-caption);
  color: var(--dim);
  max-width: var(--measure);
}

.callout-marker {
  border-left-color: var(--severe);
  color: var(--ink);
}

/* --- CTA ------------------------------------------------------------------- */

.cta {
  padding: var(--s-6) 0;
  border-top: 1px solid var(--rule-strong);
}

.cta h2 {
  max-width: 16ch;
  margin-bottom: var(--s-4);
}

/* The closing call to action on its own dark ground: one heading, one button,
   centred, and nothing else competing. */
.cta[data-theme] {
  padding: var(--s-7) 0;
  border-top: 0;
  text-align: center;
}

.cta[data-theme] h2 {
  max-width: 20ch;
  margin-left: auto;
  margin-right: auto;
  font-size: var(--t-display);
}

.cta[data-theme] .lede,
.cta[data-theme] p {
  max-width: 46ch;
  margin-left: auto;
  margin-right: auto;
}

.cta[data-theme] .hero-actions,
.cta[data-theme] .cta-actions {
  justify-content: center;
  margin-bottom: 0;
}

/* --- Lead-capture modal ----------------------------------------------------
   Styles for the modal built by assets/lead-form.js. It is the ONE lead
   mechanism on the site; the raw urlencoded form that used to sit on the two
   homepages has been removed in favour of it. */

.lead-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: none;
  opacity: 0;
  transition: opacity 240ms var(--ease-out);
}

/* Two classes, two jobs: `open` is the display switch and `is-open` carries
   the motion. The split is what gives the transition a start state — the node
   is displayed at opacity 0 with its card 16px low for one frame, and only
   then does lead-form.js add `is-open` and let it settle. Collapsing the two
   into one class means the modal is already at its final values on the first
   painted frame and nothing animates. */
.lead-modal.open {
  display: block;
}

.lead-modal.is-open {
  opacity: 1;
}

.lead-modal-backdrop {
  position: absolute;
  inset: 0;
  background: var(--black);
  opacity: 0.55;
}

.lead-modal-card {
  position: relative;
  transform: translateY(16px);
  transition: transform 240ms var(--ease-out);
  max-width: 30rem;
  margin: 6vh auto 0;
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  padding: var(--s-4);
  max-height: 88vh;
  overflow-y: auto;
}

.lead-modal-card h3 {
  font-family: var(--font-serif);
  font-size: var(--t-section);
  margin-bottom: var(--s-2);
}

.lead-modal-sub {
  font-size: var(--t-caption);
  color: var(--dim);
  margin-bottom: var(--s-4);
}

.lead-modal-close {
  position: absolute;
  top: var(--s-2);
  right: var(--s-2);
  background: none;
  border: 0;
  font-size: 1.6rem;
  line-height: 1;
  color: var(--mid);
  cursor: pointer;
  padding: var(--s-1);
}

.lead-modal-close:hover {
  color: var(--ink);
}

.lead-form label {
  display: block;
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mid);
  margin-bottom: var(--s-3);
}

.lead-form input:not([type="hidden"]):not([name="website"]),
.lead-form textarea {
  display: block;
  width: 100%;
  margin-top: var(--s-1);
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--rule-strong);
  border-radius: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--t-body);
  text-transform: none;
  letter-spacing: 0;
}

.lead-form input:focus,
.lead-form textarea:focus {
  outline: none;
  border-color: var(--teal);
}

.lead-form-submit {
  width: 100%;
  justify-content: center;
  margin-top: var(--s-1);
}

.lead-form-note {
  margin: var(--s-3) 0 0;
  font-size: var(--t-caption);
  line-height: 1.55;
}

.lead-form-note.err {
  color: var(--severe);
}

/* --- Case-study extras ----------------------------------------------------- */

.timeline-date-main {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
}

.timeline-date-sub {
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mid);
  margin-top: var(--s-1);
}

.timeline-entry-after .timeline-date-main,
.timeline-entry-after .timeline-body h3 {
  color: var(--mid);
}

.pull {
  font-family: var(--font-serif);
  font-size: var(--t-section);
  line-height: 1.35;
  color: var(--ink);
  max-width: 40ch;
  padding-left: var(--s-3);
  border-left: 2px solid var(--rule-strong);
  margin: var(--s-4) 0;
}

/* ===========================================================================
   THE REBRAND LAYER — 2026-09-02
   Everything below was added for "The record, read.": the dark hero and its
   resolve piece, the stat band, the method track, the case-study timeline, and
   the motion states the JavaScript in chrome.js and hero.js drives.

   Two rules hold throughout: no colour literal ever appears here (tokens only,
   and tokens.css is the single place a hex may live under site/), and every
   moving thing has a finished state that is reached with the script dead or
   with prefers-reduced-motion set.
   =========================================================================== */

/* --- Hero on a dark ground -------------------------------------------------
   The hero opens every page. On its own ground it takes extra air above,
   because the sticky nav sits over it. Above 62rem the copy holds the left
   column and the resolve piece the right; below that the piece stacks under. */

.hero[data-theme] {
  padding-top: calc(var(--s-7) + var(--s-4));
}

.hero-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-5);
  align-items: center;
}

.hero-copy {
  min-width: 0;
}

@media (min-width: 62rem) {
  .hero-grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
    gap: var(--s-5);
  }
}

/* --- The resolve piece -----------------------------------------------------
   An inline <svg viewBox="0 0 640 400"> authored by WP4 in index.html and
   da/index.html. Every colour arrives through `color:` on these classes with
   stroke="currentColor" or fill="currentColor" in the markup, so the piece
   inverts with the section like everything else.

   Sizes are px against the 640x400 viewBox: the subject name is 22, the ghost
   names 16, mono labels 11.

   The DEFAULT state of every class below is the FINISHED frame. Only
   `.js .hero-piece:not(.is-done)` hides or un-draws anything, so with no
   JavaScript, with the script broken, or with reduced motion the reader gets
   the whole piece at once and nothing is invisible. */

.hero-piece {
  display: block;
  width: 100%;
  max-width: 40rem;
  aspect-ratio: 16 / 10;
  overflow: visible;
  color: var(--ink);
}

.hp-namesakes,
.hp-subject,
.hp-rules,
.hp-findings,
.hp-sources,
.hp-signature {
  fill: none;
}

.hp-name {
  font-family: var(--font-serif);
  font-size: 22px;
  fill: currentColor;
  color: var(--ink);
}

.hp-name--ghost {
  font-size: 16px;
  color: var(--mid);
}

.hp-id,
.hp-date,
.hp-source,
.hp-punch {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  fill: currentColor;
  color: var(--dim);
}

.hp-finding {
  font-family: var(--font-sans);
  font-size: 13px;
  fill: currentColor;
  color: var(--ink);
  transition: color var(--d-fast) var(--ease-out);
}

.hp-rule,
.hp-connector,
.hp-sign-rule {
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.hp-rule {
  color: var(--rule-strong);
}

.hp-connector {
  color: var(--rule);
  transition: color var(--d-fast) var(--ease-out);
}

/* The one element per piece that carries the finding gets the oxblood. */
.hp-punch--severe {
  color: var(--severe);
}

.hp-sign {
  font-family: var(--font-serif);
  font-size: 16px;
  fill: currentColor;
  color: var(--ink);
}

.hp-sign-rule {
  color: var(--teal);
}

/* Pre-animation states. Lines carry pathLength="1", so one dash of length 1
   offset by 1 is an undrawn line whatever its real length. */
.js .hero-piece:not(.is-done) .hp-rule,
.js .hero-piece:not(.is-done) .hp-connector,
.js .hero-piece:not(.is-done) .hp-sign-rule {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}

.js .hero-piece:not(.is-done) .hp-finding,
.js .hero-piece:not(.is-done) .hp-punch,
.js .hero-piece:not(.is-done) .hp-name--ghost {
  opacity: 0;
}

/* Hover reads one finding at a time: the row under the pointer brightens, the
   others step back to --mid, and the connector inside that row goes teal. */
.hp-findings:hover .hp-finding:not(:hover),
.hp-findings:focus-within .hp-finding:not(:focus-within) {
  color: var(--mid);
}

.hp-finding:hover,
.hp-finding:focus-within {
  color: var(--ink);
}

.hp-finding:hover .hp-connector,
.hp-finding:focus-within .hp-connector,
.hp-connector.is-linked {
  color: var(--teal);
}

@media (max-width: 40rem) {
  .hp-namesakes,
  .hp-source {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .js .hero-piece .hp-rule,
  .js .hero-piece .hp-connector,
  .js .hero-piece .hp-sign-rule {
    stroke-dasharray: none;
    stroke-dashoffset: 0;
  }

  .js .hero-piece .hp-finding,
  .js .hero-piece .hp-punch,
  .js .hero-piece .hp-name--ghost {
    opacity: 1;
  }

  .hp-finding,
  .hp-connector {
    transition: none;
  }
}

/* --- Stat band -------------------------------------------------------------
   The documentary device on a dark section: four figures, hairline-separated,
   each with one line of caption. Wraps to two columns on a phone. */

.stat-band {
  display: flex;
  flex-wrap: wrap;
  border-top: 1px solid var(--rule);
}

.stat-band > * {
  flex: 1 1 11rem;
  min-width: 0;
  padding: var(--s-4) var(--s-4) var(--s-4) var(--s-3);
  border-left: 1px solid var(--rule);
}

.stat-band > *:first-child {
  border-left: 0;
  padding-left: 0;
}

.stat-figure {
  font-family: var(--font-serif);
  font-size: clamp(2.2rem, 4vw, 3.2rem);
  line-height: 1;
  color: var(--black);
}

.stat-note {
  font-size: var(--t-caption);
  line-height: 1.45;
  color: var(--dim);
  max-width: 18ch;
  margin-top: var(--s-2);
}

@media (max-width: 40rem) {
  .stat-band > * {
    flex: 1 1 45%;
    padding: var(--s-3);
    border-left: 0;
    border-top: 1px solid var(--rule);
  }

  .stat-band > *:first-child {
    padding-left: var(--s-3);
  }
}

/* --- Method track ----------------------------------------------------------
   The five steps sit against a rail that fills with reading position. Where
   scroll-driven animation exists the rail fills itself; where it does not,
   chrome.js adds .is-tracked and the same fill runs on a transition. */

.steps--tracked {
  position: relative;
  padding-left: var(--s-4);
}

.steps--tracked::before,
.steps--tracked::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 1px;
}

.steps--tracked::before {
  background: var(--rule);
}

.steps--tracked::after {
  background: var(--ink);
  transform: scaleY(1);
  transform-origin: top;
}

/* The fallback reads chrome.js's --progress (0 to 1, published on the
   container as the reader descends it); .is-tracked is the coarser hook for a
   caller that only wants the rail filled. */
.js .steps--tracked::after {
  transform: scaleY(var(--progress, 0));
  transition: transform 120ms linear;
}

.js .steps--tracked.is-tracked::after {
  transform: scaleY(1);
}

@supports (animation-timeline: view()) {
  .js .steps--tracked::after {
    transition: none;
    animation: previo-fill-y linear both;
    animation-timeline: view();
    animation-range: entry 60% cover 85%;
  }
}

.step.is-active .step-num {
  color: var(--ink);
}

@keyframes previo-fill-y {
  from {
    transform: scaleY(0);
  }
  to {
    transform: scaleY(1);
  }
}

@media (max-width: 40rem) {
  .steps--tracked {
    padding-left: var(--s-3);
  }
}

@media (prefers-reduced-motion: reduce) {
  .js .steps--tracked::after {
    transform: scaleY(1);
    transition: none;
    animation: none;
  }
}

/* --- The case-study timeline ----------------------------------------------
   The showpiece. A vertical hairline runs the length of the record and draws
   as the reader descends it; entries are a mono date column against the body;
   the flag is TALLY STROKES and a word, never a coloured chip. */

.timeline {
  position: relative;
  padding-left: var(--s-4);
}

.timeline::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--rule-strong);
  transform: scaleY(1);
  transform-origin: top;
}

.js .timeline::before {
  transform: scaleY(0);
  transition: transform 1200ms var(--ease-out);
}

.js .timeline.is-drawn::before {
  transform: scaleY(1);
}

@supports (animation-timeline: view()) {
  .js .timeline::before {
    transition: none;
    animation: previo-fill-y linear both;
    animation-timeline: view();
    animation-range: entry 90% cover 70%;
  }
}

.timeline-header {
  padding-bottom: var(--s-3);
}

.timeline-header h2,
.timeline-header h3 {
  margin-bottom: var(--s-2);
}

.timeline-header p {
  font-size: var(--t-caption);
  color: var(--dim);
  max-width: 58ch;
  margin: 0;
}

/* The single most severe entry on the page. It is the only place the oxblood
   takes area, which is what makes it read as a finding rather than a style. */
.timeline-entry--severe .timeline-date-main,
.timeline-entry--severe .timeline-flag {
  color: var(--severe);
}

.timeline-entry--severe {
  border-left: 2px solid var(--severe);
  margin-left: calc(var(--s-4) * -1);
  padding-left: calc(var(--s-4) - 2px);
}

.timeline-flag {
  display: inline-block;
  margin-top: var(--s-2);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink);
}

/* Tally strokes, not colour: one hairline for caution, two for do-not-proceed.
   The second stroke is the right border of the same pseudo-element, so there
   is no extra element and no dot, pill or chip anywhere near it. */
.timeline-flag::before {
  content: "";
  display: inline-block;
  width: 0;
  height: 0.8em;
  margin-right: 0.6em;
  vertical-align: -0.08em;
  border-left: 1px solid currentColor;
}

.flag-danger::before {
  width: 3px;
  border-right: 1px solid currentColor;
}

.narrative {
  max-width: 58ch;
  color: var(--dim);
}

.narrative h2,
.narrative h3,
.narrative strong {
  color: var(--black);
}

/* --- Aftermath grid --------------------------------------------------------
   Four hairline-topped cells. The value is display serif in true black; the
   label is the caption under it. */

.aftermath-cell {
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule-strong);
}

.aftermath-cell-value {
  font-family: var(--font-serif);
  font-size: 2.2rem;
  line-height: 1.05;
  color: var(--black);
  margin-bottom: var(--s-2);
}

.aftermath-cell-label {
  font-size: var(--t-caption);
  line-height: 1.5;
  color: var(--dim);
}

/* --- Legacy label variants -------------------------------------------------
   .label-dark and .label-light predate data-theme and are being removed from
   the markup by WP5 and WP6. Until then they must render exactly like .label
   rather than as unstyled body text. */

.label-dark,
.label-light {
  font-size: var(--t-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--mid);
  margin: 0 0 var(--s-3);
  font-weight: 500;
}

/* --- Lead modal, continued -------------------------------------------------
   The card rises 16px into place on `is-open`, one frame after `open` put it
   on screen — the same two-step as the backdrop's fade above. */

.lead-modal.is-open .lead-modal-card {
  transform: none;
}

/* Honeypot: reachable by a bot, invisible and out of the way for a person.
   Replaces the inline style string lead-form.js used to inject. */
.lead-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

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

  /* No transition to give a start state to, so `open` alone is enough: the
     modal is at its final values on the frame it appears. */
  .lead-modal.open {
    opacity: 1;
  }

  .lead-modal-card {
    transform: none;
  }

  .js .timeline::before {
    transform: scaleY(1);
    transition: none;
    animation: none;
  }

  .register tbody tr,
  .register td:first-child,
  .step-num,
  .study-title {
    transition: none;
  }
}

/* --- Print -----------------------------------------------------------------
   A printed page has no scroll and no script timeline, so every progressive
   state resolves to its finished frame. The !important here matches the print
   block in base.css: it is the one place the cascade is overruled outright. */

@media print {
  .js .timeline::before,
  .js .steps--tracked::after {
    transform: scaleY(1) !important;
    animation: none !important;
    transition: none !important;
  }

  .js .hero-piece .hp-rule,
  .js .hero-piece .hp-connector,
  .js .hero-piece .hp-sign-rule {
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
  }

  .js .hero-piece .hp-finding,
  .js .hero-piece .hp-punch,
  .js .hero-piece .hp-name--ghost {
    opacity: 1 !important;
  }

  .lead-modal {
    display: none !important;
  }
}

/* ===========================================================================
   Landing page v2: the thread, the tiles, the specimen frame, the access beat.

   Same two rules as everything above. No colour literal (tokens only), and
   every moving thing has a finished frame that is reached with the script dead
   or with prefers-reduced-motion set.
   =========================================================================== */

/* --- The thread ------------------------------------------------------------
   One oxblood hairline that runs the length of the page and draws with reading
   position: a spine in the left gutter, a tick into every anchor, a small
   square node at each tick that fills as the line reaches it.

   Oxblood is the finding colour and this is the path a finding travels, which
   is the only reason the accent is allowed a second use.

   The svg is built by chrome.js as the last child of `.thread-scope`, so with
   scripting off there is no thread at all, which is the right outcome for a
   decorative line. Themed sections set background and colour only and open no
   stacking context, so one positioned svg paints above every ground the page
   changes to on the way down. */

.thread-scope {
  position: relative;
}

.thread {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  overflow: visible;
  color: var(--thread-line);
}

/* The viewBox is authored in page pixels, so geometry maps one to one and the
   hairline stays a hairline whatever the height of the scope. */
.thread path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.thread-node {
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  transition: fill var(--d-fast) var(--ease-out);
}

.thread-node.is-threaded {
  fill: currentColor;
}

/* The path carries pathLength="1", so one dash of length 1 is the whole line
   whatever its real geometry, and the offset is the reading position read
   straight off `--progress`. */
.js .thread path {
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--progress, 0));
  transition: stroke-dashoffset 120ms linear;
}

/* Where scroll-driven animation exists, CSS draws the line and the JavaScript
   loop is left the cheap job of lighting the nodes. The range below is the same
   reading line the fallback computes, 60% of the viewport crossing the svg,
   written as an offset into the cover range. */
@supports (animation-timeline: view()) {
  /* The timeline is NAMED and declared on the svg rather than taken as a bare
     view() on the path, because a bare view() would make the PATH the subject
     and a path is measured by its bounding box: it starts at the full stop and
     ends at the last tick, which is about 600px shorter than the scope. That
     ran the drawn tip roughly a fifth of the page ahead of the nodes the loop
     lights, so the line arrived somewhere before the mark did. The svg spans
     the scope exactly, which is what the JavaScript formula measures too. */
  .js .thread {
    view-timeline-name: --previo-thread;
  }

  .js .thread path {
    transition: none;
    animation: previo-draw-thread linear both;
    animation-timeline: --previo-thread;
    animation-range: cover 40vh cover calc(100% - 60vh);
  }
}

@keyframes previo-draw-thread {
  from {
    stroke-dashoffset: 1;
  }
  to {
    stroke-dashoffset: 0;
  }
}

/* --- Product tiles ---------------------------------------------------------
   Three whole-tile links under the headline. The big label is the plain
   description, the small mono label is the product name, and each carries a
   micro-visual in the brand grammar.

   The DEFAULT state of every piece class below is the FINISHED frame: nothing
   here is hidden waiting for a script. chrome.js plays the assembly once on
   reveal and again on hover or focus, and that is all it does. */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--s-4);
  margin-top: var(--s-5);
}

.tile {
  display: block;
  color: inherit;
  text-decoration: none;
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule-strong);
  transition: border-color var(--d-fast) var(--ease-out);
}

.tile:hover,
.tile:focus-visible {
  border-color: var(--ink);
  text-decoration: none;
}

/* The global teal outline already applies; the tile only asks for more room,
   because at the default offset the outline sits on the visual. */
.tile:focus-visible {
  outline-offset: 6px;
}

.tile-visual {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 1;
  overflow: visible;
  color: var(--ink);
  margin-bottom: var(--s-3);
}

.tile h3 {
  font-family: var(--font-sans);
  font-size: var(--t-body);
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: 0;
  color: var(--ink);
  margin-bottom: var(--s-2);
}

.tile p {
  font-size: var(--t-caption);
  color: var(--dim);
  margin-bottom: var(--s-2);
}

.tile-ref {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--mid);
}

/* Piece parts. Colour arrives through `color:` on these classes with
   stroke="currentColor" or fill="currentColor" in the markup, so a piece
   inverts with its section like everything else. Sizes are px against the
   viewBox the tile authors. */

.tp-rule,
.tp-connector {
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* A drawn part is fully drawn at rest. chrome.js plays it from offset 1. */
.tp-draw {
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
}

.tp-box {
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  color: var(--rule-strong);
}

.tp-mono {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  fill: currentColor;
  color: var(--dim);
}

.tp-name {
  font-family: var(--font-serif);
  font-size: 14px;
  fill: currentColor;
}

.tp-ghost {
  color: var(--mid);
}

.tp-severe {
  color: var(--severe);
}

/* --- The specimen frame ----------------------------------------------------
   A white PDF page cannot sit straight on the ground: on paper it dissolves
   into it, and on a dark section it burns a hole. So every render sits on an
   ivory matte inside a hairline, and the matte flips with the tokens. */

/* Each matte wraps its own render. Without this the grid stretches every cell
   to the tallest, and a wide short crop next to a full page sits in a column of
   empty ivory that reads as a loading failure. */
.specimens {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--s-4);
  align-items: start;
}

.specimen {
  margin: 0;
  padding: var(--s-3);
  background: var(--ivory);
  border: 1px solid var(--rule);
}

.specimen img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--rule);
}

.specimen-caption {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: 0.08em;
  line-height: 1.5;
  color: var(--dim);
  margin-top: var(--s-2);
}

/* --- The access beat -------------------------------------------------------
   Short serif lines on a dark ground, one to a row, hairline-separated. No
   bullets: a bullet would make it a feature list, and these are statements. */

.access-lines {
  list-style: none;
  margin: var(--s-4) 0 0;
  padding: 0;
  max-width: 46ch;
}

.access-lines li {
  margin: 0;
  padding: var(--s-3) 0;
  border-top: 1px solid var(--rule);
  font-family: var(--font-serif);
  font-size: var(--t-lede);
  line-height: 1.45;
  color: var(--ink);
}

.access-lines li:first-child {
  padding-top: 0;
  border-top: 0;
}

/* --- A hero with no piece beside it ----------------------------------------
   The resolve piece moved to the method page, so the home hero is one column.
   Without this the 62rem rule above holds a second column open and squeezes the
   headline into half the page. The modifier is for markup that wants to say so
   outright; the :has() form catches a hero that simply has one child. */

.hero-grid--single,
.hero-grid:has(> .hero-copy:only-child) {
  grid-template-columns: minmax(0, 1fr);
}

@media (prefers-reduced-motion: reduce) {
  .js .thread path {
    stroke-dashoffset: 0;
    transition: none;
    animation: none;
  }

  .thread-node,
  .tile {
    transition: none;
  }
}

/* The thread is reading furniture and a printed page has no reading position,
   so it goes rather than freezing at whatever fraction the reader stopped on. */
@media print {
  .thread {
    display: none;
  }
}
