/* ============================================================================
   Moduloa — editorial front end
   ---------------------------------------------------------------------------
   This is NOT the northinvent "Draft 3" stylesheet. It is Moduloa's own file,
   written dark-first against styles/tokens.css. What it borrows from that draft
   is only compositional: a large display scale, a 12-column grid with a bleed
   escape, numbered markers, and generous vertical air. Everything else —
   palette, typefaces, the inverted band, the figure slots — is Moduloa's.

   Three deliberate departures from the reference design:

   1. DARK IS THE PAPER. The reference is white paper with one dark band for
      pacing. Moduloa's ground is #111312, so the pacing device INVERTS: the one
      band on a long page is LIGHT. Same job, opposite polarity.

   2. THERE ARE NO PHOTOGRAPHS. The reference is image-led; this site owns one
      logo. So every figure slot is a DATA PLATE — a ruled panel of real
      sourced numbers. That is what this brand actually has, and a spec plate
      suits an instrument better than a stock photo would.

   3. LABELS ARE MONO. --font-mono (JetBrains Mono) carries every small
      uppercase label, because the technical-label voice is this brand's most
      distinctive typographic signal. Sizes already agreed: the reference label
      is 11px and so is --label-size.

   Contrast, computed against #111312 and against the light band, not guessed:
     --ed-body   #E7EBE8 on ink      15.50:1  (13.84 on alt, 12.15 on raised)
     --ed-quiet  #9BA39F on ink        7.22:1  (6.45 on alt, 5.67 on raised)
     --ed-accent #4FC99A on ink        9.02:1  (ink-on-accent 9.02, hover 10.81)
     --ed-rule-strong                  3.33:1  (clears the 3:1 non-text floor)
     ghost numerals (1.26:1) and the hairline rule (1.44:1) are decorative and
     the numerals are aria-hidden; nothing legible is ever set in them.
     On the light band: ink on paper 18.65:1, --ed-band-accent #306E56 6.02:1.
   ========================================================================= */

/* ---------------------------------------------------------------- 1. tokens */

.ed {
  /* grounds — three steps, all from the brand ramp */
  --ed-paper: var(--mo-black);          /* #111312 the page */
  --ed-paper-alt: var(--mo-ink);        /* #1B1F1D the second stock */
  --ed-raised: var(--mo-surface);       /* #242A27 panels */
  --ed-rule: rgba(255, 255, 255, 0.13);
  --ed-rule-strong: rgba(255, 255, 255, 0.36); /* 3.33:1 on ink — SC 1.4.11 floor for a link/card rule; 0.28 measured 2.49:1 and failed */
  --ed-ghost: rgba(255, 255, 255, 0.09); /* the big numerals — decorative only */

  /* ink */
  --ed-ink: var(--mo-white);
  --ed-body: var(--mo-text);
  --ed-quiet: var(--mo-muted);

  /* accent — punctuation, never decoration. Roughly four uses per view. */
  --ed-accent: var(--mo-accent);
  --ed-accent-soft: rgba(79, 201, 154, 0.12);

  /* the one inverted (light) band */
  --ed-band-paper: #ffffff;
  --ed-band-ink: var(--mo-black);
  --ed-band-body: #4a4a4a;
  --ed-band-quiet: #636363;
  --ed-band-rule: #dbdbdb;
  --ed-band-accent: #306e56;            /* accent 50% + ink, 6.02:1 on white */

  /* measure + rhythm */
  --ed-max: 1440px;
  --ed-gutter: clamp(1.25rem, 5.2vw, 6.5rem);
  --ed-col-gap: clamp(0.75rem, 1.6vw, 1.75rem);
  --ed-section: clamp(3.5rem, 8vw, 8rem);
  /* How far a bleeding figure must travel to reach the viewport edge.
     CORRECTED 2026-08-22, and the correction is a box-sizing bug, not a taste
     call. This sheet never sets box-sizing, so content-box is in force (the
     border-box rule lives in site.css and the homepage is compat={false}).
     .ed-wrap therefore has max-width: --ed-max on its CONTENT box, and its OUTER
     width is --ed-max + 2 * --ed-gutter. The old expression subtracted only
     --ed-max, so it over-travelled by exactly one gutter — 99.84px at 1920 —
     which the 5rem rail lane could not absorb: measured, the band figure's right
     edge landed at 1932 against a 1905 client width, putting the maturity
     rubric's POINTS column both off-screen and under the chapter rail.
     Correct travel = (100vw - (max + 2*gutter))/2 + gutter, and the gutter
     cancels, leaving (100vw - --ed-max)/2. Floored at --ed-gutter so narrow
     screens still bleed to their gutter. 100vw includes the scrollbar, so this
     still over-travels by half a scrollbar (~7.5px) — harmless, because the rail
     lane absorbs it. If --ed-max, --ed-gutter or box-sizing ever change, this is
     the line that has to be re-derived. */
  --ed-bleed: max(var(--ed-gutter), calc((100vw - var(--ed-max)) / 2));
  /* Lane the chapter rail reserves on the right, taken from the one bleeding
     figure. Derived, not guessed: the rail's left edge sits at
     viewport - scrollbar(15) - offset(8) - width(38) = viewport - 61, and the
     figure's right edge lands at viewport - lane. So the lane must exceed 61px.
     3rem (48px) left the figure 7px over the rail at 1680. 5rem clears it. */
  --ed-rail-lane: 5rem;

  /* ---- chrome + hero slack, DERIVED so they cannot drift ------------------
     The masthead's REAL height: twice its own padding clamp, plus the wordmark's
     line box (1.05rem x 1.65 = 27.7px, taken as 1.75rem), plus its 1px rule.
     Rendered it is 92.7px at 1920 and 1440 — the literal `5.4rem` the hero used
     to subtract reserves only 86.4px, so it was 6.3px short.

     That error compounded with a second one. Nothing this page loads sets
     box-sizing (border-box lives only in site.css:21, and index.astro is
     compat={false}), so the hero's min-height was sizing the CONTENT box and its
     80px of padding-block was added on top. Together: the hero measured 1073.6px
     starting at y=92.7 in a 1080 viewport, so 86.3px of it hung BELOW the fold —
     and at 1440x900 the four counters sat 25.8px under it. Measuring the hero's
     HEIGHT against the viewport hides this; you have to measure its BOTTOM.

     --ed-hero-stack is the hero's content height with no title break, derived
     from the type scale rather than snapshotted: the title is two nowrap lines at
     line-height 0.84, so 1.68 x --ed-display (1.68 x 112 = 188.2, measured), and
     the rest of the stack measures 505-509px. 32rem is the deliberate round-UP,
     because over-estimating withholds air while under-estimating would spend air
     that is not there. --ed-hero-slack goes negative on short screens by design;
     every consumer clamps it at 0. */
  --ed-chrome-pad: clamp(1.1rem, 2.4vw, 2rem);
  --ed-chrome: calc(2 * var(--ed-chrome-pad) + 1.75rem + 1px);
  --ed-hero-pad: calc(clamp(1.1rem, 2.2vw, 2rem) + clamp(1.5rem, 3.5vw, 3rem));
  --ed-hero-stack: calc(32rem + 1.68 * var(--ed-display));
  --ed-hero-slack: calc(100svh - var(--ed-chrome) - var(--ed-hero-pad) - var(--ed-hero-stack));

  /* type scale — this is the part that changes the page most. The old site
     topped out at 62px; the display line now reaches 136px. */
  /* Cap MEASURED by rendering, not guessed: at 1440px the title column is
     ~1170px and the indented second line has ~1018px to work in. 8.5rem
     clipped "without markup." off the right edge; 7rem clears it. Re-measure
     if the words change — the nowrap will clip rather than wrap. */
  --ed-display: clamp(2.4rem, 8.4vw, 7rem);
  --ed-h2: clamp(1.6rem, 3.2vw, 2.85rem);
  --ed-h3: clamp(1.05rem, 1.5vw, 1.4rem);
  --ed-lede: clamp(1.0625rem, 1.4vw, 1.3125rem);
  --ed-numeral: clamp(3rem, 8vw, 7.5rem);
  --ed-label: 11px;
  --ed-small: 13px;

  --ed-ease: cubic-bezier(0.2, 0, 0.1, 1);
}

/* ------------------------------------------------------------ 2. base */

.ed {
  margin: 0;
  background: var(--ed-paper);
  color: var(--ed-body);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* element defaults in :where() so they carry zero specificity — every
   component class below is a single class and therefore always wins */
.ed :where(img, svg, video) { max-width: 100%; }
.ed :where(img, video) { display: block; height: auto; }
.ed :where(h1, h2, h3, h4) {
  font-family: var(--font-display);
  font-weight: 700;               /* Space Grotesk ships 500-700; 700 is real */
  color: var(--ed-ink);
  margin: 0;
  text-wrap: balance;
}
.ed :where(p, dl, dd, figure, blockquote) { margin: 0; }
.ed :where(ul, ol) { margin: 0; padding: 0; list-style: none; }
.ed :where(a) { color: inherit; }
.ed :where(table) { border-collapse: collapse; }

.ed :focus-visible {
  outline: 2px solid var(--ed-accent);
  outline-offset: 3px;
  border-radius: 2px;
}
.ed main:focus, .ed main:focus-visible { outline: none; }

.ed-skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  padding: 0.75rem 1.25rem;
  background: var(--ed-accent); color: var(--mo-black);
  font-family: var(--font-mono); font-size: var(--ed-small);
  letter-spacing: 0.08em; text-transform: uppercase; text-decoration: none;
}
.ed-skip:focus { left: 1rem; top: 1rem; }

.ed-vh {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ------------------------------------------------- 3. layout primitives */

.ed-wrap {
  max-width: var(--ed-max);
  margin-inline: auto;
  padding-inline: var(--ed-gutter);
}
.ed-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--ed-col-gap);
}
.ed-section { padding-block: var(--ed-section); }
.ed-section--tight { padding-block: clamp(2.25rem, 4.5vw, 4rem); }
.ed-section--alt { background: var(--ed-paper-alt); }

/* --------------------------------------------------- 4. type primitives */

.ed-label {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--ed-label);
  line-height: 1.2;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ed-accent);
  margin: 0;
}
.ed-label--quiet { color: var(--ed-quiet); }

/* the numbered marker: a ghost numeral + a mono label. Numbering earns its
   place here — this brand numbers everything it owns (MOD-01..MOD-11, P-26,
   dated records), so a numeral is native rather than decorative. */
.ed-marker {
  display: flex;
  align-items: baseline;
  gap: clamp(0.7rem, 1.8vw, 1.6rem);
  margin: 0 0 clamp(1.1rem, 2.4vw, 2rem);
}
.ed-marker__num {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--ed-numeral);
  line-height: 0.74;
  letter-spacing: -0.05em;
  color: var(--ed-ghost);
  margin-left: -0.055em;   /* optical: pull the sidebearing off the text edge */
  user-select: none;
}
.ed-marker__label { padding-bottom: 0.4em; }

/* ------------------------------------------------------- 5. links + CTA */

.ed-cta {
  display: inline-flex; align-items: center; gap: 0.7em;
  padding: 0.95em 1.9em;
  border: 0; border-radius: 999px;
  background: var(--ed-accent);
  color: var(--mo-black);            /* ink on accent = 6.5:1 */
  font-family: var(--font-mono); font-weight: 500;
  font-size: var(--ed-small); letter-spacing: 0.12em;
  text-transform: uppercase; text-decoration: none;
  transition: background-color 200ms var(--ed-ease), transform 200ms var(--ed-ease);
}
.ed-cta:hover { background: var(--mo-accent-strong); }
.ed-cta__arrow { transition: transform 200ms var(--ed-ease); }
.ed-cta:hover .ed-cta__arrow { transform: translateX(3px); }

.ed-link {
  display: inline-flex; align-items: center; gap: 0.55em;
  padding-bottom: 0.3em;
  border-bottom: 1px solid var(--ed-rule-strong);
  color: var(--ed-ink);
  font-family: var(--font-mono); font-weight: 500;
  font-size: var(--ed-small); letter-spacing: 0.1em;
  text-transform: uppercase; text-decoration: none;
  transition: border-color 200ms var(--ed-ease), color 200ms var(--ed-ease);
}
.ed-link:hover { border-bottom-color: var(--ed-accent); color: var(--ed-accent); }
.ed-link .ed-cta__arrow { transition: transform 200ms var(--ed-ease); }
.ed-link:hover .ed-cta__arrow { transform: translateX(3px); }

/* --------------------------------------------------------- 6. masthead */

.ed-masthead {
  /* identical computed value — now shared with --ed-chrome so the masthead and
     the hero's min-height subtraction can never disagree again */
  padding-block: var(--ed-chrome-pad);
  border-bottom: 1px solid var(--ed-rule);
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--ed-paper) 88%, transparent);
  backdrop-filter: blur(8px);
}
.ed-masthead__inner {
  display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;
}
.ed-wordmark {
  font-family: var(--font-display); font-weight: 700;
  font-size: 1.05rem; letter-spacing: -0.01em;
  color: var(--ed-ink); text-decoration: none;
}
.ed-nav ul {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: clamp(0.85rem, 1.8vw, 1.8rem);
}
.ed-nav a {
  font-family: var(--font-mono); font-size: var(--ed-label);
  letter-spacing: 0.14em; text-transform: uppercase; text-decoration: none;
  color: var(--ed-quiet); padding-bottom: 2px;
  border-bottom: 1px solid transparent;
  transition: color 160ms var(--ed-ease), border-color 160ms var(--ed-ease);
}
.ed-nav a:hover { color: var(--ed-ink); border-bottom-color: var(--ed-accent); }
.ed-nav a[aria-current='page'] { color: var(--ed-ink); border-bottom-color: var(--ed-ink); }

/* WCAG 2.5.8 asks 24px of any standalone pointer target. Inline links inside a
   sentence are exempt; nav items are not. Measured at 375px these were 106x17
   and the 中文 switcher was 25x17 — the worst target on the site. Padding only,
   so the resting desktop layout is untouched: the masthead's own padding-block
   already exceeds this, which is why it is scoped to coarse pointers. */
@media (pointer: coarse) {
  .ed-nav a {
    padding-block: 0.4rem; padding-bottom: calc(0.4rem + 2px);
    /* 2.5.8 wants 24px in BOTH dimensions, and padding-block only fixed height.
       The shortest labels are the language switches — "EN" measured 16px wide on
       /zh, and "中文" 25px. This was `padding-inline: 0.35rem`, which reached the
       floor by widening EVERY item, including "Manufacturing", already far past
       it. The waste was invisible while the nav held four items and decisive at
       five, when /predictions joined the row.
       THE FLOOR IS IN PX, NOT REM, ON PURPOSE: --ed-label is a fixed 11px and no
       rule sets a root font-size, so the label text does NOT scale with the
       reader's default font size while a rem floor would. Measured at 375px with
       html{font-size:12px}, `min-width: 1.625rem` gave the worst target ("EN" on
       /zh) 19.5px wide — WORSE than the 23.6px the padding gave. 26px is the same
       guarantee at every root size (26 x 33.9 at root 16px), and none of the
       width. min-width works here only because `.ed-nav li { display: flex }`
       below blockifies the anchor; keep the two declarations in this one query.
       Known cost: border-bottom spans the padding box, so the aria-current
       underline is now flush with the label instead of overhanging it by ~5px. */
    min-width: 26px;
    text-align: center;
  }
  .ed-nav li { display: flex; }
}

/* ------------------------------------------------------------- 7. hero */

/* The hero is a FIRST SCREEN, not merely a tall block. Measured at 1425x900 the
   original spacing put the data plate 17px past the fold, and at the founder's
   own 1853x884 it clipped the last KPI row. Padding and gaps are tightened here
   and the plate is compacted below; together that reclaims ~150px and lands the
   whole plate in frame. 100svh, not vh, so mobile browser chrome is counted. */
.ed-hero {
  position: relative;
  isolation: isolate;
  padding-top: clamp(1.1rem, 2.2vw, 2rem);
  padding-bottom: clamp(1.5rem, 3.5vw, 3rem);
  overflow-x: clip;
}
/* Only claim the screen where there IS a screen to claim: below 640px tall the
   min-height would force scrolling for no gain. */
@media (min-width: 768px) and (min-height: 640px) {
  .ed-hero {
    /* box-sizing here is REQUIRED, not cosmetic. Without it min-height sized the
       content box and the 80px of padding-block was added on top — 993.6 + 32 +
       48 = 1073.6, the measured height — and subtracting a masthead 6.3px shorter
       than the real one added the rest. Net: 86.3px of hero below the fold at
       1080, 86px at 900. These two lines fix both halves.
       NOTE: --ed-hero-pad mirrors the two padding clamps above. Change them
       together or the slack calculation silently drifts. */
    box-sizing: border-box;
    min-height: calc(100svh - var(--ed-chrome));
    display: flex;
    align-items: center;
  }
  .ed-hero > .ed-wrap { width: 100%; }
}
/* Row-gap is now the TIGHTEST joint in the hero, because unaided its only job is
   to bind the kicker to the title it labels. Before, this hero had five joints at
   25/24/30/30/24 — a 6px spread, so nothing grouped — and they were INVERTED:
   .ed-hero__col's own gap computed to 30.4px while the row-gap separating that
   whole block from a 188px headline computed to 24px. The bigger breaks are now
   bought with margins by the items that START a group (steps below), because a
   grid's row-gap is uniform and this hero needs three different intervals. */
.ed-hero__grid { row-gap: clamp(0.5rem, 1vw, 0.75rem); }
/* The one decorative layer in the system, and it is here because the brand
   mark earns it. aria-hidden, pointer-events off, and behind every grid item.
   Clipped by the hero's own overflow-x, so it can never widen the page. */
.ed-watermark {
  position: absolute;
  z-index: -1;
  right: clamp(-6rem, -4vw, -1rem);
  top: 50%;
  translate: 0 -50%;
  width: clamp(26rem, 46vw, 52rem);
  max-width: none;
  /* 0.09 is the treatment the brand already uses for this mark on the live
     hero; 0.055 read as almost nothing. Contrast over it is sampled from the
     rendered pixels below, not assumed. */
  opacity: 0.09;
  pointer-events: none;
  user-select: none;
}
@media (max-width: 767px) {
  .ed-watermark { width: 145vw; right: -40vw; opacity: 0.07; }
}

.ed-hero__kicker { grid-column: 1 / -1; }
.ed-hero__title {
  grid-column: 1 / -1;
  font-size: var(--ed-display);
  line-height: 0.84;
  letter-spacing: -0.035em;
  /* THE ONE MAJOR BREAK. 24px under a 94px line box is 0.26em — the worst joint
     in the hero, and the reason a screen with unused height still read as
     crowded. This spends ONLY --ed-hero-slack, so it can never push content
     off-screen: where there is no free space it evaluates to 0 and the layout is
     identical to a hero without it. The cap is the lesser of 4.5rem and 0.75x the
     display line, so the break stays PROPORTIONAL — generous under 112px type,
     modest under 38px type on a phone — instead of a flat value that would be
     1.9em on mobile. Deliberately no floor: a 1.25rem floor spent 20px that does
     not exist at 1184x832 and pushed the counters below the fold. */
  margin-bottom: clamp(0px, var(--ed-hero-slack), min(4.5rem, 0.75 * var(--ed-display)));
}
/* Break the two lines BY HAND and never let them wrap on their own — the
   second line steps in and drops to the quiet ink. That off-axis step is the
   whole gesture. Re-measure the break if the words change: Space Grotesk is
   narrower than the reference face at the same size. */
.ed-hero__line { display: block; white-space: nowrap; }
/* Was var(--ed-quiet) — the APPARATUS tier. --ed-quiet appears 25 times in this
   file and carries the kicker, the honesty disclaimer, the plate labels and notes,
   the spec labels, the captions and the rail. Against #111312 the ladder is
   #fff 18.65:1, --ed-body 15.50:1, --ed-quiet 7.22:1 — so the site's entire
   proposition was set in its own footnote voice, which is why twenty quiet
   elements could compete with a 112px headline. --ed-body is one step below white
   rather than three tiers below it, and the off-axis gesture is already carried by
   the 13% indent. The accent is NOT available here: this view already spends its
   four uses on the CTA ground, the plate's label, $5,900 and the accented 29. */
.ed-hero__line--b { color: var(--ed-body); }

/* The left column is ONE grid item holding lede + status + actions. It used to
   be three items with the plate spanning all three rows — and because the plate
   is taller than the prose, the grid distributed that extra height across the
   spanned rows and opened dead air under the lede. One cell beside one cell
   cannot do that. */
.ed-hero__col {
  grid-column: 1 / -1;
  display: grid;
  /* Was clamp(1.2rem, 2.4vw, 1.9rem) = 30.4px — LARGER than the 24px row-gap that
     separated this whole block from the headline, so a group that should read as
     one bound argument read as three loose fragments. It has to be the smallest
     interval in the hero after the kicker's. */
  gap: clamp(0.85rem, 1.6vw, 1.25rem);
  align-content: start;
}
.ed-hero__lede {
  font-size: var(--ed-lede);
  line-height: 1.5;
  color: var(--ed-body);
  max-width: 46ch;
}
/* the honest status sits in the SAME block as the claim — separated, honesty
   reads as a disclaimer */
.ed-hero__status {
  font-size: 0.9375rem; line-height: 1.6;
  color: var(--ed-quiet);
  max-width: 54ch;
  padding-left: 1rem;
  border-left: 2px solid var(--ed-rule-strong);
}
.ed-hero__actions {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: clamp(0.9rem, 2vw, 1.75rem);          /* horizontal — unchanged */
  /* The invitation is not the fourth paragraph of the argument. Paired with the
     tighter column gap above this is a pure REDISTRIBUTION and costs no height:
     the column was 24 + 24 = 48px between its three members, and is now
     20 + 28 = 48px. */
  margin-top: 0.5rem;
}

/* THE DATA PLATE — this slot holds a figure on the reference design. Here it
   holds real sourced numbers, because that is what the site has. */
.ed-plate-data {
  grid-column: 1 / -1;
  border: 1px solid var(--ed-rule);
  background: var(--ed-paper-alt);
  /* The next four rules tighten the plate's INTERNAL rhythm and reclaim ~39px
     without removing one word. Load-bearing, not cosmetic: hero row 3 is
     max(.ed-hero__col, .ed-plate-data) and the PLATE governs — measured 417px
     against a prose column that only needs 318px at 1440 — which is why every
     previous prose-tightening pass failed to move the fold. At 1440x900 the stack
     budget and the stack were both 775.6px, i.e. ZERO slack, so without this the
     title break cannot open at that size at all.
     A 1-line clamp on .ed-kpi__note was tested and REJECTED: at 1440 the note box
     is 474px and ALL FOUR notes need two lines, so it would silently cut the
     runner-up price, the four named customers, the source and the wheeled-model
     caveat. Rhythm is free; provenance is not. */
  padding: clamp(0.9rem, 1.7vw, 1.15rem);
  display: grid; gap: 0.45rem;
  align-content: start;
}
.ed-plate-data__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; padding-bottom: 0.55rem;
  border-bottom: 1px solid var(--ed-rule);
}
.ed-plate-data__stamp {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.1em;
  color: var(--ed-quiet); white-space: nowrap;
}
.ed-kpi { display: grid; gap: 0.4rem; }
.ed-kpi__row {
  /* the 0.2rem row gap binds each note to the key it sources — they are one
     unit, and 6.4px had them reading as two */
  display: grid; grid-template-columns: 1fr auto; gap: 0.2rem 1rem;
  align-items: baseline;
  padding-bottom: 0.35rem; border-bottom: 1px solid var(--ed-rule);
}
.ed-kpi__row:last-child { border-bottom: 0; padding-bottom: 0; }
.ed-kpi__k {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--ed-quiet);
}
.ed-kpi__v {
  font-family: var(--font-display); font-weight: 700;
  font-size: 1.02rem; color: var(--ed-ink);
  font-variant-numeric: tabular-nums; text-align: right;
}
.ed-kpi__v--accent { color: var(--ed-accent); }
.ed-kpi__note {
  grid-column: 1 / -1;
  font-size: 0.8125rem;
  line-height: 1.4;
  color: var(--ed-quiet);
  /* two lines maximum, so the plate cannot grow unpredictably tall and push
     itself off the first screen when a source note runs long. DO NOT drop this to
     1: at 1440 the note box is 474px and every one of the four notes needs two
     lines, so a 1-line clamp truncates all four sources. */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}

/* a figure caption WRAPS — __stamp is nowrap because it holds a short date, and
   reusing it on a sentence pushed the caption off the right edge */
.ed-caption {
  display: block;
  margin-top: 0.7rem;
  font-family: var(--font-mono);
  font-size: 10.5px;
  line-height: 1.65;
  letter-spacing: 0.06em;
  color: var(--ed-quiet);
  max-width: 60ch;
}

/* the spec sheet — small facts on a hard rule, the reference's one device that
   maps perfectly onto a pre-revenue status block */
.ed-spec {
  grid-column: 1 / -1;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 8.5rem), 1fr));
  gap: 0.8rem 2rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--ed-rule-strong);
}
.ed-spec dt {
  font-family: var(--font-mono); font-size: var(--ed-label);
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ed-quiet); margin-bottom: 0.35rem;
}
.ed-spec dd {
  margin: 0; font-family: var(--font-display); font-weight: 700;
  font-size: 1.35rem; letter-spacing: -0.01em; color: var(--ed-ink);
  font-variant-numeric: tabular-nums;
}
.ed-spec dd small {
  display: block; font-family: var(--font-body); font-weight: 400;
  font-size: 0.8125rem; letter-spacing: 0; color: var(--ed-quiet);
  margin-top: 0.2rem;
}
.ed-spec > div { min-width: 0; }
.ed-spec__accent dd { color: var(--ed-accent); }

/* The four counters are a FOOTER to the whole hero, not a fifth paragraph of it,
   so they get a group break on top of the row-gap. Scoped with `>` to the hero
   grid so .ed-spec keeps `margin: 0` everywhere else; specificity (0,2,0) beats
   the base (0,1,0), and both /index and /labs/specimen match. */
.ed-hero__grid > .ed-spec { margin-top: clamp(0.75rem, 1.6vw, 1.4rem); }

/* --------------------------------------------------------- 8. the index */

.ed-index { border-top: 1px solid var(--ed-rule-strong); padding-top: 1.1rem; }
.ed-index__head { grid-column: 1 / -1; margin-bottom: 1rem; }
.ed-index__list {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 1fr));
  column-gap: var(--ed-col-gap);
}
.ed-index__item { border-top: 1px solid var(--ed-rule); }
.ed-index__link {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 0.75rem; padding: 0.8rem 0;
  text-decoration: none; color: var(--ed-ink); font-size: 0.9375rem;
  transition: color 160ms var(--ed-ease);
}
.ed-index__link:hover { color: var(--ed-accent); }
.ed-index__n {
  font-family: var(--font-mono); font-size: var(--ed-label);
  letter-spacing: 0.1em; color: var(--ed-quiet);
}

/* ------------------------------------------------------- 9. statement */

.ed-statement__marker { grid-column: 1 / -1; }
.ed-statement__body { grid-column: 1 / -1; }
.ed-statement__text {
  font-family: var(--font-display); font-weight: 700;
  font-size: var(--ed-h2); line-height: 1.14; letter-spacing: -0.02em;
  color: var(--ed-ink); max-width: 30ch;
}
.ed-statement__sub {
  margin-top: 1.15rem; font-size: 1rem; line-height: 1.65;
  color: var(--ed-body); max-width: 58ch;
}
.ed-statement__actions { margin-top: clamp(1.5rem, 3vw, 2.5rem); }

/* 01 HAS TO ARRIVE, NOT ACCUMULATE. The position used to open on a hedge set at
   --ed-h2 with the whole argument in one 66-word block under it, so the first
   thing after the hero was a paragraph. It is now two short declaratives set
   between --ed-h2 and --ed-display: loud enough to be the second thing a
   skimmer reads, small enough that it never competes with the h1.
   The cap is 4.5rem, not 7rem: at 1440 the h1's line box is 112px and this one
   is 72px, so the hierarchy survives at every width the display line is capped
   at. No max-width — the measure is the grid column, and a ch cap on top of
   that only makes the break unpredictable.

   4.8vw is MEASURED, not chosen, and the measurement is in EM OF COLUMN, not in
   characters — character count is actively misleading here. The break has to
   hold, because a first line that wraps turns two declaratives into three
   ragged ones. .ed-statement__body is 8 of 12 columns and every input scales on
   vw, so the budget is nearly flat: 12.065em of column at a 768px viewport,
   12.116em at 960, 12.157em at 1184, 12.189em at 1440. THE PINCH IS THE LOW
   END, not 1440 — check 768, not the widest window you have.
   Against that budget, measured by cloning .ed-statement__line and reading
   client rects:
     "Made where it is needed."     11.325em   6.1% clear   <- BINDING
     "Not for cheap labour."         9.600em  20.4% clear
     在有需求的地方制造。            9.699em  19.6% clear
     而不是为了廉价劳动力。         10.669em  11.6% clear
   Rejected on width, all of them accurate but too long: "Not where labour is
   cheap." 12.073em (0.4%), "Not chasing cheap labour." 11.873em (1.6%), "Not
   where wages are low." 11.507em (4.6%), 而不是人力成本最低的地方。 12.610em
   (wraps). 5.2vw instead of 4.8vw pushes the binding line to 12.27em, over
   budget at every width.
   RE-MEASURE IF EITHER LINE CHANGES, in either language: 6.1% is ~27px at the
   pinch, which holds against rasterisation but not against a copy edit. The
   font is a webfont, so metrics are stable across platforms; if it fails to
   load the line WRAPS rather than clips, which is the safe direction. */
.ed-statement__text--loud {
  font-size: clamp(2rem, 4.8vw, 4.5rem);
  line-height: 1.02;
  letter-spacing: -0.03em;
  max-width: none;
}
/* Two blocks, not one balanced paragraph: the break is the argument's turn and
   must not move. `white-space: nowrap` is deliberately NOT copied from
   .ed-hero__line — that line is two words and this one is five, so it has to be
   allowed to wrap on a phone rather than clip. */
.ed-statement__line { display: block; }
/* The turn, in accent, because that is where the argument pivots and this
   sheet's accent is punctuation. It takes the section's resting accent count
   from two (the mono chapter label, one inline link) to three, inside the
   "roughly four per view" budget in §1. Back it off with one token —
   var(--ed-body) — if it reads loud; nothing else depends on the colour. */
.ed-statement__line + .ed-statement__line { color: var(--ed-accent); }

/* an accent link INSIDE running prose — not the uppercase mono .ed-link */
.ed-inline {
  color: var(--ed-accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}
.ed-inline:hover { color: var(--mo-accent-strong); }

/* ------------------------------------------------------- 10. article */

.ed-article { overflow-x: clip; }
.ed-article__grid { row-gap: clamp(1.4rem, 2.8vw, 2.5rem); }
.ed-article__marker { grid-column: 1 / -1; }
.ed-article__head { grid-column: 1 / -1; }
.ed-article__title {
  font-size: var(--ed-h2); line-height: 1.1; letter-spacing: -0.022em;
  max-width: 26ch;
}
.ed-article__body { grid-column: 1 / -1; }
.ed-article__standfirst {
  font-size: var(--ed-lede); line-height: 1.55; color: var(--ed-body);
  max-width: 58ch;
}
.ed-article__body p + p { margin-top: 0.95rem; max-width: 62ch; }
.ed-article__actions { margin-top: clamp(1.4rem, 2.8vw, 2.25rem); }
.ed-article__figure { grid-column: 1 / -1; margin: 0; }

/* 02's DEMOTED evidence. The currency argument used to be the whole of section
   02; it is now a footnote under that section's primary table, because
   /manufacturing/economics and the 2026-07-30 journal note carry it in full.
   It lives INSIDE .ed-article__figure on purpose: no grid placement of its own,
   so it inherits the figure's column at every width and needs no entry in the
   @media (min-width: 768px) block below, where .ed-article__figure takes its
   grid-row: 2 / span 2. The only job left is the vertical gap that separates a
   demoted block from the table it sits under. */
.ed-article__aside { margin-top: clamp(1.6rem, 3vw, 2.4rem); }
/* .ed-label has margin: 0, so without this the mono label sits flush on the
   demoted table. */
.ed-article__aside .ed-label { margin-bottom: 0.7rem; }

/* a ruled data table, since editorial layouts have no table of their own and
   this site's real content IS tables */
.ed-table-wrap { overflow-x: auto; border: 1px solid var(--ed-rule); }
.ed-table { width: 100%; min-width: 30rem; font-size: 0.9rem; }
.ed-table th {
  text-align: left; font-family: var(--font-mono); font-weight: 500;
  font-size: 10.5px; letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--ed-quiet); padding: 0.7rem 0.85rem;
  border-bottom: 1px solid var(--ed-rule-strong); white-space: nowrap;
  background: var(--ed-paper-alt);
}
.ed-table td {
  padding: 0.68rem 0.85rem; border-bottom: 1px solid var(--ed-rule);
  color: var(--ed-body); vertical-align: top;
}
.ed-table tr:last-child td { border-bottom: 0; }
.ed-table .n { font-family: var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--ed-ink); }
.ed-table .accent { color: var(--ed-accent); }

/* ------------------------------ 11. the band — LIGHT, because we are dark */

.ed-band {
  background: var(--ed-band-paper);
  color: var(--ed-band-body);
  border-top: 1px solid var(--ed-accent);
  border-bottom: 1px solid var(--ed-accent);
  overflow-x: clip;
}
.ed-band__inner {
  padding-block: clamp(3rem, 7vw, 6.5rem);
  row-gap: clamp(1.5rem, 3vw, 2.5rem);
  align-items: center;
}
.ed-band__marker { grid-column: 1 / -1; }
.ed-band__marker .ed-marker__num { color: rgba(17, 19, 18, 0.12); }
.ed-band__marker .ed-label { color: var(--ed-band-accent); }
.ed-band__body { grid-column: 1 / -1; }
.ed-band__title {
  font-size: var(--ed-h2); line-height: 1.1; letter-spacing: -0.022em;
  color: var(--ed-band-ink); max-width: 24ch;
}
.ed-band__text {
  margin-top: 1rem; max-width: 52ch; color: var(--ed-band-body);
}
.ed-band__figure { grid-column: 1 / -1; margin: 0; }
.ed-band__actions {
  grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center;
  gap: clamp(1rem, 2.4vw, 2rem); margin-top: 0.5rem;
}
/* everything inside the band flips without restating a colour at the call site */
.ed-band .ed-link { color: var(--ed-band-ink); border-bottom-color: rgba(17, 19, 18, 0.3); }
.ed-band .ed-link:hover { color: var(--ed-band-accent); border-bottom-color: var(--ed-band-accent); }
.ed-band .ed-table-wrap { border-color: var(--ed-band-rule); }
.ed-band .ed-table th { color: var(--ed-band-quiet); background: #eeeeee; border-bottom-color: #bfbfbf; }
.ed-band .ed-table td { color: var(--ed-band-body); border-bottom-color: var(--ed-band-rule); }
.ed-band .ed-table .n { color: var(--ed-band-ink); }
.ed-band .ed-table .accent { color: var(--ed-band-accent); }
.ed-band .ed-plate-data { background: #f6f6f6; border-color: var(--ed-band-rule); }
.ed-band .ed-plate-data__head { border-bottom-color: var(--ed-band-rule); }
.ed-band .ed-kpi__row { border-bottom-color: var(--ed-band-rule); }
.ed-band .ed-kpi__k, .ed-band .ed-kpi__note, .ed-band .ed-plate-data__stamp { color: var(--ed-band-quiet); }
.ed-band .ed-kpi__v { color: var(--ed-band-ink); }
.ed-band .ed-kpi__v--accent { color: var(--ed-band-accent); }
/* A figcaption in the band. .ed-caption resolves to --ed-quiet, a DARK-ground
   ink measuring ~2.6:1 on the band's white paper — the one member of this
   override family nobody had needed yet, because no caption had ever sat in a
   band. Section 03's revision line is the first. verify-build check 3 cannot
   catch this: it asks whether a class HAS a rule, not whether the rule is
   readable on the ground it lands on. */
.ed-band .ed-caption { color: var(--ed-band-quiet); }
.ed-band :focus-visible { outline-color: var(--ed-band-accent); }

/* ---- SECTION 03's HUMANOID MATURITY INDEX — gauge + rubric ---------------
   It lives in the band, so every colour here is a LIGHT-ground token. The
   dark-ground ones fail outright: .ed-label's own --ed-accent (#4FC99A)
   measures 2.07:1 on white, which is why .ed-mi__label is overridden rather
   than left to inherit — the same trap .ed-band .ed-caption documents above.
   Note the pre-existing .ed-band__marker .ed-label rule does NOT cover this
   one, which sits in .ed-band__figure, so the override is required not
   redundant.

   Contrast on #ffffff, computed:
     .ed-mi__num, tbody th, tfoot   --ed-band-ink    18.65:1
     .ed-mi__of                     --ed-band-body    8.86:1
     .ed-mi__label, note link       --ed-band-accent  6.02:1
     .ed-mi__note                   --ed-band-quiet   6.01:1
     thead th (inherited, unchanged) --ed-band-quiet on #eeeeee 5.18:1
     fill #306E56 on track #DBDBDB                     4.35:1 — clears the 3:1
       non-text floor even though the bar is aria-hidden, and therefore
       decorative, because its value is printed as text beside it.

   NO MOTION, DELIBERATELY. The fill has no transition and no animation, so
   prefers-reduced-motion has nothing to switch off (§16 neutralises transitions
   across .ed * regardless). A bar that grew on load would also mean the bar and
   the printed number disagreed for a few hundred milliseconds, which on this
   page is a correctness bug rather than a missing nicety.

   375px: the figure is full-bleed below 768, so the wrap gets the whole 375.
   The rubric's min-width is 22rem (352px) rather than .ed-table's 30rem, so it
   fits a phone without a horizontal scroll; the ladder table below it keeps
   30rem and keeps scrolling, exactly as it does today. */
.ed-mi { display: grid; gap: 0.55rem; }
.ed-band .ed-mi__label { color: var(--ed-band-accent); }
.ed-mi__read { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.75rem; }
.ed-mi__num {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(2.2rem, 4.4vw, 3rem); line-height: 0.9;
  letter-spacing: -0.03em; font-variant-numeric: tabular-nums;
  color: var(--ed-band-ink);
}
.ed-mi__of {
  font-size: 0.9375rem; line-height: 1.4;
  color: var(--ed-band-body); max-width: 30ch;
}
.ed-mi__track { display: block; height: 8px; background: var(--ed-band-rule); }
.ed-mi__fill { display: block; height: 100%; background: var(--ed-band-accent); }
.ed-table.ed-mi__table { min-width: 22rem; }
.ed-mi__note { font-size: 0.8125rem; line-height: 1.5; color: var(--ed-band-quiet); }
.ed-mi__note a { color: var(--ed-band-accent); }
/* The ladder table sits under the index note, so it needs the gap a demoted
   block gets in section 02 — same clamp as .ed-article__aside, same reason. The
   class also exists to carry this section's interior snap stop; see §15. */
.ed-band__ladder { margin-top: clamp(1.6rem, 3vw, 2.4rem); }
/* The ladder answers the founder's question — "who is winning the humanoid race"
   — so it says so out loud instead of hiding it in an .ed-vh caption. The heading
   sits OUTSIDE the .ed-table-wrap on purpose: that wrap is overflow-x: auto, so a
   heading inside it would scroll sideways away from its own table on a phone. It
   also means .ed-band__ladder is now a plain container rather than the scroller,
   which is what lets its interior snap stop land ON the heading rather than below
   it. Scoped to this one block, not `.ed-band .ed-label`, which would have made
   the pre-existing .ed-band__marker .ed-label rule redundant. --ed-band-accent is
   6.02:1 on the band's white paper; --ed-accent would be 2.07:1. */
.ed-band__ladder-head {
  color: var(--ed-band-accent);
  margin-bottom: 0.7rem;
  /* Aligns the heading with the table's first COLUMN, not with the wrap's
     border. .ed-table th/td carry 0.85rem of horizontal padding and the wrap adds
     a 1px border, so this puts the label text within a pixel of the column
     headers under it. It is also load-bearing at 375px: .ed-band__figure is the
     one element on the page that bleeds to the viewport edge, so without this the
     heading's text measured flush at x=0 while the table's own cell padding kept
     its text at x=15 — the heading alone touching the screen edge. */
  padding-inline: 0.85rem;
}
/* ROW headers wrap, COLUMN headers do not. .ed-table th is nowrap, which is
   right for the four short column labels and wrong for row headers, which are
   phrases: an unscoped `white-space: normal` here let auto table layout squeeze
   the last column until the Chinese header 贡献分 broke across two lines. */
.ed-band .ed-mi__table :is(tbody, tfoot) th { white-space: normal; }
/* The dimension column is a row header for the screen reader and prose for the
   eye: no mono, no uppercase, no header ground. Four grey nowrap label cells
   read as a second header row and cost the reading column its width. */
.ed-band .ed-mi__table tbody th {
  background: transparent; font-family: inherit; font-size: 0.9rem;
  letter-spacing: 0; text-transform: none; color: var(--ed-band-ink);
  border-bottom: 1px solid var(--ed-band-rule); vertical-align: top;
}
/* The total: a rule above it, no header ground, nothing under it. */
.ed-band .ed-mi__table tfoot th,
.ed-band .ed-mi__table tfoot td {
  background: transparent; border-bottom: 0;
  border-top: 1px solid var(--ed-band-rule); color: var(--ed-band-ink);
}

/* ------------------------------------------------------- 12. chapters */

.ed-chapter { padding-block: var(--ed-section); border-top: 1px solid var(--ed-rule); overflow-x: clip; }
.ed-chapter__grid { row-gap: clamp(1.2rem, 2.5vw, 2.25rem); }
.ed-chapter__marker { grid-column: 1 / -1; }
.ed-chapter__marker .ed-marker__num { font-size: clamp(2.4rem, 5vw, 4.75rem); line-height: 0.8; }
.ed-chapter__title { font-size: var(--ed-h2); line-height: 1.08; letter-spacing: -0.024em; }
.ed-chapter__aside { grid-column: 1 / -1; }
.ed-chapter__intro { font-size: 1rem; line-height: 1.65; color: var(--ed-body); max-width: 52ch; }
.ed-chapter__actions { margin-top: 1.2rem; }
.ed-chapter__cards { grid-column: 1 / -1; }

/* cards carry NO image frame — the reference put a photo in every one and this
   site has none. A card is a title, a mono meta line, and an excerpt. */
.ed-cards {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--ed-col-gap);
  row-gap: clamp(1.5rem, 3vw, 2.5rem);
}
.ed-cards > * { grid-column: span 12; }
.ed-card {
  display: flex; flex-direction: column; gap: 0.55rem;
  align-self: start;
  padding-top: 1rem;
  border-top: 1px solid var(--ed-rule-strong);
  text-decoration: none; color: inherit;
  transition: border-color 200ms var(--ed-ease);
}
.ed-card:hover { border-top-color: var(--ed-accent); }
.ed-card__title {
  font-size: var(--ed-h3); line-height: 1.25; letter-spacing: -0.01em;
  transition: color 160ms var(--ed-ease);
}
.ed-card:hover .ed-card__title { color: var(--ed-accent); }
.ed-card__meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.4em 0.7em;
  font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--ed-quiet);
}
.ed-card__meta span + span::before {
  content: '·'; margin-right: 0.6em; color: var(--ed-rule-strong);
}
.ed-card__excerpt {
  font-size: 0.9375rem; line-height: 1.6; color: var(--ed-body); max-width: 44ch;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden;
}
.ed-card__more {
  margin-top: 0.15rem;
  font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--ed-quiet);
  display: inline-flex; align-items: center; gap: 0.5em;
}
.ed-card:hover .ed-card__more { color: var(--ed-accent); }

/* ------------------------------------------- 12b. the rotating journal group */

/* One card swaps per tick. Height is pinned so the row cannot jump as a shorter
   note replaces a longer one — a reflowing card row is worse than a static one. */
.ed-cards--rotate .ed-card {
  min-height: 12.5rem;
  transition: opacity 380ms var(--ed-ease), transform 380ms var(--ed-ease);
}
.ed-cards--rotate .ed-card.is-leaving { opacity: 0; transform: translateY(-6px); }
.ed-cards--rotate .ed-card.is-entering { opacity: 0; transform: translateY(6px); }

.ed-rotator__bar {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: 1rem;
  margin-top: 1.4rem; padding-top: 0.9rem;
  border-top: 1px solid var(--ed-rule);
}
.ed-rotator__meta {
  font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--ed-quiet);
}
/* WCAG 2.2.2 wants a control for auto-updating content. Hidden until the script
   arms it, so a no-JS reader is never shown a button that does nothing. */
.ed-rotator__toggle {
  display: inline-flex; align-items: center; gap: 0.5em;
  padding: 0.4em 0.9em;
  background: transparent;
  border: 1px solid var(--ed-rule-strong);
  border-radius: 999px;
  color: var(--ed-quiet);
  font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: 0.13em; text-transform: uppercase;
  cursor: pointer;
  transition: color 160ms var(--ed-ease), border-color 160ms var(--ed-ease);
}
.ed-rotator__toggle:hover { color: var(--ed-ink); border-color: var(--ed-accent); }
.ed-rotator__toggle[aria-pressed='true'] { color: var(--ed-accent); border-color: var(--ed-accent); }

@media (prefers-reduced-motion: reduce) {
  /* the script does not auto-rotate under reduced motion; kill the transition
     too so a manual swap is instant rather than a slow fade */
  .ed-cards--rotate .ed-card { transition: none; }
}

/* ---------------------------------------------------------- 13. a note */

.ed-note { background: var(--ed-paper-alt); }
.ed-note__grid { row-gap: 1.1rem; }
.ed-note__label { grid-column: 1 / -1; }
.ed-note__body { grid-column: 1 / -1; }
.ed-note__text {
  font-size: var(--ed-lede); line-height: 1.5; color: var(--ed-ink); max-width: 44ch;
}
.ed-note__actions { grid-column: 1 / -1; }

/* ------------------------------------- 13b. the planned slate (/labs/academy) */

/* A planned slate is not a published one, and the page must not let those read
   the same. Planned cards are outlined rather than ruled, and carry a status
   chip instead of a date. The landing page ends on the ask now; these rules are
   loaded by /labs/academy, beside the research that argues for the slate, and by
   /labs/specimen, which still carries its own copy of the old 05 section. */
.ed-card--planned {
  border-top-color: var(--ed-rule);
  padding: 1rem 1.1rem;
  border: 1px dashed var(--ed-rule-strong);
}
.ed-card--planned:hover { border-top-color: var(--ed-rule-strong); }
.ed-chip {
  align-self: flex-start;
  padding: 0.25em 0.65em;
  border: 1px solid currentColor;
  /* 10.5px is this system's mono-label floor; 9.5 was below it, not a step in it */
  font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ed-quiet);
}
.ed-chip--next { color: var(--ed-accent); }
.ed-card--planned .ed-card__title { font-size: 1.08rem; }
.ed-card--planned .ed-card__excerpt { -webkit-line-clamp: 4; }
.ed-evidence {
  font-family: var(--font-mono); font-size: 10px; line-height: 1.6;
  letter-spacing: 0.05em; color: var(--ed-quiet);
  padding-top: 0.6rem; margin-top: 0.15rem;
  border-top: 1px solid var(--ed-rule);
}

/* -------------------------------------------- 13c. the seats (the closing) */

/* The landing page ends on /onboard's open seats, reusing .ed-card unchanged.
   The one thing that does not carry over is the clamp: a seat description from
   the register runs a line longer than a journal excerpt. At 1184x832 — the
   narrowest viewport where scroll-snap is armed — the span-4 track computes to
   ~341px, inside .ed-card__excerpt's 44ch cap, so the longest seat description
   (004) needs four lines; at three, the offer clause ("...and gets manufacturing
   optionality in return") is cut off, which is the half of the sentence that
   makes the seat worth taking. Four lines, the same allowance
   .ed-card--planned takes directly above. It costs height only where it is
   needed: at 1440 the 44ch cap binds instead and three lines suffice. */
.ed-card--seat .ed-card__excerpt { -webkit-line-clamp: 4; }

/* ---------------------------------------------------------- 14. footer */

.ed-footer {
  border-top: 1px solid var(--ed-rule-strong);
  padding-block: clamp(2.25rem, 4.5vw, 4.5rem) clamp(2rem, 4vw, 3.5rem);
}
.ed-footer__grid { row-gap: clamp(1.75rem, 3.5vw, 2.75rem); }
.ed-footer__brand { grid-column: 1 / -1; display: grid; gap: 0.6rem; align-content: start; }
.ed-footer__tagline {
  font-family: var(--font-mono); font-size: var(--ed-label);
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--ed-quiet);
}
.ed-footer__email { color: var(--ed-accent); text-decoration: none; font-size: 0.9375rem; }
.ed-footer__email:hover { text-decoration: underline; text-underline-offset: 0.2em; }
.ed-footer__col { grid-column: 1 / -1; }
.ed-footer__coltitle {
  font-family: var(--font-mono); font-weight: 500; font-size: var(--ed-label);
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--ed-quiet);
  margin-bottom: 1rem;
}
.ed-footer__links { display: grid; gap: 0.65rem; }
.ed-footer__links a {
  font-size: 0.9375rem; text-decoration: none; color: var(--ed-ink);
  border-bottom: 1px solid transparent; justify-self: start;
}
.ed-footer__links a:hover { border-bottom-color: var(--ed-accent); }
.ed-footer__legal {
  grid-column: 1 / -1;
  display: flex; flex-wrap: wrap; gap: 0.7rem 1.6rem; align-items: baseline;
  margin-top: clamp(1.4rem, 2.8vw, 2.25rem); padding-top: 1.2rem;
  border-top: 1px solid var(--ed-rule);
  font-size: var(--ed-small); color: var(--ed-quiet);
}
.ed-footer__legal a { color: var(--ed-quiet); text-decoration: none; border-bottom: 1px solid currentColor; }
.ed-footer__legal a:hover { color: var(--ed-ink); }

/* ========================================================================
   15. RESPONSIVE — the 12-column composition exists in TWO tiers, not one.
   Between 768 and 959 the page is a narrower setting of the same idea, not a
   fallback to one column. Only below 768 — a phone — does it become a scroll.
   ===================================================================== */

@media (min-width: 768px) {
  .ed-hero__kicker { grid-column: 1 / span 8; grid-row: 1; }
  .ed-hero__title  { grid-column: 1 / -1;     grid-row: 2; }
  .ed-hero__line--b { margin-left: 8%; }
  .ed-hero__col    { grid-column: 1 / span 7; grid-row: 3; }
  .ed-plate-data   { grid-column: 8 / -1;     grid-row: 3; align-self: start; }
  .ed-spec         { grid-column: 1 / -1;     grid-row: 4; }

  .ed-index__head { grid-column: 1 / span 4; margin-bottom: 0; }
  .ed-index__list { grid-column: 5 / -1; }

  .ed-statement__marker { grid-column: 1 / span 4; }
  .ed-statement__body   { grid-column: 5 / -1; }

  .ed-article__marker { grid-column: 1 / span 5; grid-row: 1; }
  .ed-article__head   { grid-column: 1 / span 6; grid-row: 2; }
  .ed-article__body   { grid-column: 1 / span 6; grid-row: 3; }
  .ed-article__figure { grid-column: 7 / -1;     grid-row: 2 / span 2; align-self: center; }

  .ed-band__marker { grid-column: 1 / span 6; grid-row: 1; }
  .ed-band__body   { grid-column: 1 / span 6; grid-row: 2; }
  .ed-band__actions{ grid-column: 1 / span 6; grid-row: 3; }
  .ed-band__figure { grid-column: 7 / -1;     grid-row: 1 / span 3; align-self: center;
                     margin-right: calc(-1 * var(--ed-bleed)); }

  .ed-note__label { grid-column: 1 / span 4; }
  .ed-note__body  { grid-column: 5 / -1; }

  .ed-chapter__marker { grid-column: 1 / span 7; grid-row: 1; }
  .ed-chapter__aside  { grid-column: 8 / -1;     grid-row: 1; }
  .ed-chapter__cards  { grid-column: 1 / -1;     grid-row: 2; margin-top: clamp(0.75rem, 2vw, 1.75rem); }
  .ed-cards > .ed-card { grid-column: span 6; }

  .ed-footer__brand { grid-column: 1 / -1; }
  .ed-footer__col--nav     { grid-column: 1 / span 5; }
  .ed-footer__col--about   { grid-column: 7 / span 6; }
}

@media (min-width: 960px) {
  .ed-hero__kicker { grid-column: 1 / span 6; }
  .ed-hero__title  { grid-column: 1 / span 11; }
  .ed-hero__line--b { margin-left: 13%; }
  .ed-hero__col    { grid-column: 1 / span 6; }
  .ed-plate-data   { grid-column: 8 / -1; }

  .ed-index__head { grid-column: 1 / span 3; }
  .ed-index__list { grid-column: 4 / -1; }

  .ed-statement__marker { grid-column: 1 / span 3; }
  .ed-statement__body   { grid-column: 4 / span 8; }

  .ed-chapter__marker { grid-column: 1 / span 6; }
  .ed-chapter__aside  { grid-column: 7 / span 5; }
  .ed-cards > .ed-card { grid-column: span 4; }

  .ed-footer__brand      { grid-column: 1 / span 4; }
  .ed-footer__col--nav   { grid-column: 6 / span 3; }
  .ed-footer__col--about { grid-column: 9 / span 4; }
}

@media (max-width: 767px) {
  /* the display line must never overflow a phone, so the nowrap yields here */
  .ed-hero__line { white-space: normal; }
  .ed-band__figure { margin-inline: calc(-1 * var(--ed-gutter)); }
}

/* ------------------------------------------- 15. chapter rail + snap ---- */

/* Anchor jumps used to land UNDER the sticky masthead — there was no
   scroll-padding anywhere. /projects and /projects.html both 308 to /#projects,
   so this was a live defect, not a hypothetical. Generous by ~10px so a snapped
   or jumped heading clears the blurred bar instead of touching it. */
:root { scroll-padding-top: 6.5rem; }

/* SNAP: `mandatory`, made safe by giving every long section interior stops.
 *
 * `mandatory` is what "scroll and lock" actually means — every rest position is
 * a defined stop, not just the ones near a boundary. The reason it is usually a
 * mistake is that a section taller than the scrollport has no snap point in its
 * middle, so the browser drags the reader back to a boundary and the middle
 * becomes physically unreachable. That is the real defect, not `mandatory`
 * itself.
 *
 * So instead of cutting copy to make sections fit, the long ones get a second
 * snap point on a REAL content block. Measured at 1440x900 (796px usable after
 * the sticky masthead): only two sections exceeded it — the hero at 893 and the
 * article at 974. The interior stops below bring every gap between consecutive
 * snap points under the usable height, so nothing is unreachable and every stop
 * lands on a meaningful unit of the argument rather than an invisible spacer.
 *
 * Height gate is 52rem rather than 46rem: mandatory needs the guarantee to
 * hold, and below that height the gaps stop fitting. Under it — and on phones,
 * and under prefers-reduced-motion — there is no snapping at all. */
@media (min-width: 74rem) and (min-height: 52rem) and (prefers-reduced-motion: no-preference) {
  /* Scoped to the page that has a rail. /labs/specimen shares these ed-* section
     classes, so an unscoped rule locked its scrolling too — a page nobody asked
     to lock. The landing page is the only one that opts in. */
  html:has(.ed-rail) { scroll-snap-type: y mandatory; scroll-behavior: smooth; }

  body:has(.ed-rail) :is(.ed-hero, .ed-section, .ed-section--tight, .ed-band, .ed-chapter) {
    scroll-snap-align: start;
  }

  /* Interior stops, so `mandatory` never strands a reader mid-section. Each is a
     real content block, so a stop is a place worth stopping rather than an
     invisible spacer.
     .ed-article__aside earned its entry by measurement, not symmetry. Section 02
     now carries TWO stacked tables in a figure that is `grid-row: 2 / span 2`,
     so the figure — not the prose — sets the section's height, and the
     .ed-article__body -> #tracker gap measured 807px against 728px usable at
     1184x832. Over the scrollport, `mandatory` strands the reader: exactly the
     failure the interior stops exist to prevent. The demoted currency table is a
     real block, so stopping on it both fixes the arithmetic and lands somewhere
     worth landing.
     .ed-band__ladder earned its entry the same way, and its position was
     MEASURED rather than assumed. Section 03's figure is `grid-row: 1 / span 3`,
     so the figure — not the prose column — sets that section's height, and the
     maturity index added a gauge, a six-row rubric and a note to the top of it:
     at 1184x832 (728px usable) the .ed-band__body -> #record gap went over the
     scrollport, the exact failure `mandatory` strands a reader on.
     A stop on .ed-mi does NOT fix it. The band's inner grid is
     `align-items: center` and the figure now fills it, so .ed-mi's top sits only
     a few dozen px under the section's own start stop — a stutter, not a stop,
     and it leaves the oversized gap below it intact. The ladder table is the
     block that actually sits in the middle of the section, and it is a real
     content unit: the availability ladder is the other half of what this section
     publishes.
     .ed-statement__body LOST its entry to the same measurement. Section 01 is
     ~512px tall at 1184x832 and ~614px at its widest gated setting, both inside
     the 728px scrollport, so the stop was never buying reachability — it sat
     75px below a section stop the reader could already see whole, which made the
     page's loudest claim arrive twice and softly. Removing it merges two gaps
     into one that is still clear of 728. */
  body:has(.ed-rail) :is(
    .ed-spec,                /* hero — the four counters */
    .ed-article__body,       /* the membership argument  */
    .ed-article__aside,      /* 02's demoted currency table */
    .ed-band__body,          /* the evidence copy        */
    .ed-band__ladder,        /* 03's availability ladder */
    .ed-chapter__cards       /* the record, the seats    */
  ) {
    scroll-snap-align: start;
    /* scroll-padding-top clears the masthead for the scrollport; this keeps an
       interior stop from sitting flush against it */
    scroll-margin-top: 1.25rem;
  }

  /* THE FOOTER IS A SNAP STOP, aligned to `end`.
     Without it `mandatory` made the bottom of the page unreachable: the last
     stop sat 938px above the document end on a 900px viewport, so scrolling to
     the very bottom settled 162px short and the © and CC BY line could not be
     read at all. Measured at 1440x900 and 1024x832; 1920x1080 happened to be
     tall enough to hide the bug, which is exactly why it needed measuring. */
  body:has(.ed-rail) .ed-footer { scroll-snap-align: end; }
}

/* THE RAIL — a fixed 01…05 index of the numbered chapters.
 *
 * It floats, so it has to be given a lane rather than allowed to sit on top of
 * the page. Measuring text rectangles (not element boxes — those include cell
 * padding and hide the problem) found real copy running under it at EVERY width
 * from 1024 to 1920, by 37 to 114px: table cells reading "Unitree, AgiBot,
 * Fourier…". On a page whose entire argument is sourced numbers, a floating
 * ornament must not cover a single one of them.
 *
 * Exactly one element bleeds to the right edge — .ed-band__figure — so the lane
 * is bought back from that one rule below, rather than by padding the whole
 * page in. */
.ed-rail {
  display: none;                 /* only shown where there is room for it */
  position: fixed;
  right: 0.55rem;
  /* Sits ABOVE the vertical midline, not on it. Dead-centre right is where the
     browser itself puts things: in a founder screenshot at 1920 a Chrome
     extension button was parked at almost exactly 50% and was covering rail
     items 03 and 04. Bottom-right is no better — that is where chat and cookie
     widgets live. The upper third is the one band nothing else claims, and it
     also reads better, sitting nearer the content it indexes.
     clamp keeps it clear of the sticky masthead on short windows. */
  top: clamp(var(--ed-chrome), 50% - 7rem, 50%);
  transform: translateY(-50%);
  z-index: 40;
}
/* 74rem, not 64rem. The gutter is 5.2vw and only exceeds the rail's 61px
   footprint above ~1173px; below that, content clipped at the container edge
   ran under the rail (8px at 1024). Small laptops simply get no rail — the
   chapter numbers are still printed inline beside every section heading. */
@media (min-width: 74rem) and (min-height: 40rem) {
  .ed-rail { display: block; }

  /* The page keeps SYMMETRIC margins. An earlier version padded every
     .ed-wrap by 8rem, which pulled all homepage copy in on one side — a big,
     visible layout change to solve a small overlap.

     Measured properly, the rail only ever needed 3–19px: the sole thing that
     reaches the right edge is .ed-band__figure, which bleeds there on purpose.
     So the lane is reclaimed from that ONE element. --ed-bleed's arithmetic is
     untouched — the figure still travels exactly one gutter — it simply stops
     on the lane boundary now instead of the viewport edge. */
  .ed-band__figure { margin-right: calc(var(--ed-rail-lane) - var(--ed-bleed)); }
}

.ed-rail__list { list-style: none; margin: 0; padding: 0.35rem 0.3rem; }

/* The rail sits in the gutter, but the full-bleed figures reach past it, so it
   needs its own ground to stay legible — the same treatment the masthead uses. */
.ed-rail__list {
  background: color-mix(in srgb, var(--ed-paper) 72%, transparent);
  backdrop-filter: blur(6px);
  border: 1px solid transparent;
}
.ed-rail:hover .ed-rail__list,
.ed-rail:focus-within .ed-rail__list { border-color: var(--ed-rule); }

.ed-rail__link {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.5rem;
  padding: 0.5rem 0.35rem;
  text-decoration: none;
  color: var(--ed-quiet);
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 10.5px;
  letter-spacing: 0.14em;
  transition: color 0.25s var(--ed-ease);
}
.ed-rail__n { font-variant-numeric: tabular-nums; }

/* The active mark is drawn with ::after, not with an element, because anything
   in flow adds WIDTH — and the rail's width is exactly what has to stay inside
   the existing gutter so the page never needs re-padding. */
.ed-rail__link::after {
  content: "";
  position: absolute;
  right: -0.3rem;
  top: 50%;
  width: 2px;
  height: 0.55rem;
  background: currentColor;
  opacity: 0;
  transform: translateY(-50%) scaleY(0.4);
  transition: opacity 0.24s var(--ed-ease), transform 0.24s var(--ed-ease);
}

/* The chapter name, revealed on hover or keyboard focus. Positioned out of flow
   on purpose: a label that participates in layout would shift the whole rail
   every time the pointer crossed it. */
.ed-rail__label {
  position: absolute;
  right: calc(100% - 0.2rem);
  white-space: nowrap;
  padding: 0.2em 0.6em;
  background: var(--ed-paper-alt);
  border: 1px solid var(--ed-rule);
  color: var(--ed-body);
  text-transform: uppercase;
  opacity: 0;
  transform: translateX(0.35rem);
  pointer-events: none;
  transition: opacity 0.22s var(--ed-ease), transform 0.22s var(--ed-ease);
}
/* :focus as well as :focus-visible — a label that only appears for the
   keyboard heuristic leaves click-focus and programmatic focus with nothing. */
.ed-rail__link:hover .ed-rail__label,
.ed-rail__link:focus .ed-rail__label,
.ed-rail__link:focus-visible .ed-rail__label {
  opacity: 1;
  transform: translateX(0);
}

.ed-rail__link:hover,
.ed-rail__link:focus-visible { color: var(--ed-ink); }
.ed-rail__link:focus-visible { outline: 2px solid var(--ed-accent); outline-offset: 2px; }

/* Set by js/section-rail.js. Without JS the rail is still five working anchor
   links — it simply never highlights, which is a missing nicety, not a fault. */
.ed-rail__link[aria-current] { color: var(--ed-accent); }
.ed-rail__link[aria-current]::after { opacity: 1; transform: translateY(-50%) scaleY(1); }
.ed-rail__link[aria-current] .ed-rail__n { color: var(--ed-accent); }

@media (prefers-reduced-motion: reduce) {
  .ed-rail__link,
  .ed-rail__link::after,
  .ed-rail__label { transition: none; }
  .ed-rail__label { transform: none; }
}

/* The contact-intent line on /contact, filled in by js/contact-intent.js when a
   CTA sends someone here with ?about=. Styled by ATTRIBUTE, not by a class, so
   there is no class name to keep in sync with `verify` check 3. */
[data-contact-intent] {
  margin-top: 0.7rem;
  padding-left: 0.8rem;
  border-left: 2px solid var(--ed-accent);
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--ed-quiet);
}

/* The honest status, moved out of the hero and into "The position". Same
   treatment it had as .ed-hero__status — a quiet rule-left aside — but named for
   where it now lives, because a class called ed-hero__* sitting in a statement
   section is a lie the next reader has to untangle. */
.ed-statement__status {
  margin-top: clamp(1rem, 2vw, 1.5rem);
  padding-left: 1rem;
  border-left: 2px solid var(--ed-rule-strong);
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--ed-quiet);
  max-width: 62ch;
}

/* The three counters that ARE the three fundamentals are now doors. They read as
   labels, not links, until you go near them — a hover rule rather than a
   permanent underline, because four underlined labels in a row would read as a
   nav bar sitting under the hero. "Customers 0" stays inert: nothing to open. */
.ed-spec__link {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color 160ms var(--ed-ease), color 160ms var(--ed-ease);
}
.ed-spec__link:hover,
.ed-spec__link:focus-visible { color: var(--ed-accent); border-bottom-color: var(--ed-accent); }

/* Same WCAG 2.5.8 problem the nav has in §6, same shape of fix. These labels sit
   in a <dt>, not in a sentence, so the "inline" exception does not cover them:
   measured 109x15, 59x15 and 75x15 at 375px, all three failing the 24px floor on
   the vertical axis. padding-block alone is enough — every label already clears
   24px horizontally — and it is scoped to coarse pointers so the plate's desktop
   rhythm is untouched. The negative margin keeps the padding from pushing the
   figures apart: the target grows, the layout does not. */
@media (pointer: coarse) {
  .ed-spec__link {
    display: inline-block;
    padding-block: 0.3rem;
    margin-block: -0.3rem;
  }
}

/* ---------------------------------------------------- 16. reduced motion */

@media (prefers-reduced-motion: reduce) {
  .ed *, .ed *::before, .ed *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ------------------------------------------------------------ 17. print */

@media print {
  .ed-masthead { position: static; backdrop-filter: none; }
  .ed { background: #fff; color: #111312; }
  .ed :where(h1, h2, h3, h4) { color: #111312; }
}
