/* Normwerk, Rev. B: shared components. Every page consumes these unchanged. */

/* ---------- Header bar ---------- */
.site-head {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--paper);
  border-bottom: var(--rule-w) solid var(--hairline);
}
.site-head .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 64px;
  gap: var(--s-3);
}
.wordmark {
  font-weight: 500;
  font-size: var(--fs-16);
  text-decoration: none;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
.site-nav {
  display: flex;
  gap: var(--s-3);
  margin-left: auto;
}
.site-nav a {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  text-decoration: none;
  white-space: nowrap;
  color: var(--graphite);
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: 4px;
  border-bottom: 2px solid transparent;
  transition: color var(--t-micro) var(--ease-micro), border-color var(--t-micro) var(--ease-micro);
}
/* Rev. E: "Work" became "Case studies" so the nav matches the section it points
   at, and the extra 70-odd px of mono uppercase pushed the header CTA 22px off
   the right edge at 768 on every page. The header has always been tight here
   (see the 749 rule below, which drops the nav for the same reason). Killing the
   per-link padding buys 32px in this band, which is more than the label cost.
   The 44px tap height is on the link itself, so it is untouched. */
@media (max-width: 989px) {
  .site-nav {
    gap: var(--s-2);
  }
  .site-nav a {
    padding-inline: 0;
  }
}
.site-nav a:hover {
  color: var(--ink);
}
.site-nav a.is-urged {
  color: var(--orange-ink); /* a quiet urge past 60% depth, not a scrollspy tab */
}
@media (max-width: 749px) {
  /* Rev. C2: the whole nav goes, not just most of it. The header CTA is the
     action Contact used to stand in for, and keeping both overflowed the
     viewport by 5px at 375. Contact stays reachable from the closing band
     and the footer. */
  .site-nav {
    display: none;
  }
}

/* ---------- Chips and stamps ---------- */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  line-height: 1;
  padding: 7px 10px 6px;
  border: var(--rule-w) solid currentColor;
  white-space: nowrap;
}
.chip-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
}
.chip--approved {
  color: var(--approved);
}
.chip--review {
  color: var(--review);
}
.chip--neutral {
  color: var(--ink);
}
a.chip {
  text-decoration: none;
  min-height: 44px;
  transition: background var(--t-micro) var(--ease-micro), color var(--t-micro) var(--ease-micro);
}
a.chip--approved:hover {
  background: var(--approved);
  color: var(--paper);
}

/* ---------- Register line (section opener) ---------- */
.sec {
  padding-top: var(--sec-pad);
  position: relative;
}
/* Rev. H. --sec-gap is the distance between two SECTIONS. The header is not a
   section, it is the top edge of the sheet, so paying a full 160px gap under a
   65px sticky bar put 160px of nothing between the masthead and the first
   register mark. Max: "the spacing between the header and the hero is too big,
   feels out of place." A sheet opens on --s-5 instead. The homepage is
   unaffected: it opens on .hero, not on .sec. */
main > .sec:first-child {
  padding-top: var(--s-5);
}
.reg {
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-2);
  padding-bottom: var(--s-1);
  margin-bottom: var(--s-5);
}
.reg-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--rule-w);
  background: var(--ink);
  transform-origin: left center; /* resolved by default; motion mode draws it scaleX 0 to 1 */
}
.reg-label,
.reg-mark {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
}
.reg-mark {
  color: var(--graphite);
}
/* Rev. D: the sheet numbering is gone from the homepage. "04 · Whether this is
   for you" plus "04 / 06" numbered the same thing twice and made a sales page
   read like a drawing register. The eyebrow now names the section in one word
   and carries the accent, which is also one of the few places colour enters. */
.reg--plain {
  margin-bottom: var(--s-4);
}
.reg--plain .reg-label {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  color: var(--orange-ink);
}
.reg--plain .reg-label::before {
  content: "";
  width: 22px;
  height: var(--rule-w);
  background: var(--werk-orange);
}
.sec--invert .reg--plain .reg-label,
.band--invert .reg--plain .reg-label {
  color: var(--on-dark-orange);
}
.sec--invert .reg--plain .reg-label::before,
.band--invert .reg--plain .reg-label::before {
  background: var(--on-dark-orange);
}

/* Section heading row.
   Rev. D defect and fix: max-width was 18ch against a 64px face, so a four-word
   heading like "Four things. Nothing else." broke onto two lines while a whole
   empty half-sheet sat next to it. A heading wraps because the writer wanted it
   to, never because the measure ran out. 30ch at 48px holds every heading on
   this site in one or two deliberate lines. */
.sec-h {
  font-size: clamp(var(--fs-32), 3.2vw, var(--fs-48));
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  letter-spacing: -0.015em;
  line-height: 1.06;
  max-width: min(30ch, 100%);
  text-wrap: balance;
}
/* Opt-in for the two headings that are meant to land like a statement. */
.sec-h--lead {
  font-size: clamp(var(--fs-32), 4.2vw, var(--fs-64));
  max-width: min(22ch, 100%);
}
.sec-intro {
  margin-top: var(--s-2);
  max-width: 52ch;
}

/* ---------- CTA block ---------- */
.cta-block {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 56px;
  padding: 0 var(--s-4);
  background: var(--werk-orange);
  color: var(--ink);
  font-weight: 500;
  font-size: var(--fs-18);
  text-decoration: none;
  transition: background var(--t-micro) var(--ease-micro);
}
.cta-block:hover {
  background: var(--ink);
  color: var(--paper);
}
.cta-micro {
  margin-top: var(--s-1);
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  color: var(--graphite);
}

/* ---------- Ledger rows (ruled list rows with index) ---------- */
.ledger {
  border-top: var(--rule-w) solid var(--hairline);
}
.ledger-row {
  position: relative;
  border-bottom: var(--rule-w) solid var(--hairline);
  transition: border-color var(--t-micro) var(--ease-micro);
}
.ledger-row::before {
  content: "";
  position: absolute;
  left: 0;
  top: -1px;
  height: 2px;
  width: 0;
  background: var(--werk-orange);
  transition: width var(--t-micro) var(--ease-micro);
}
.ledger-row:hover::before,
.ledger-row:focus-within::before {
  width: var(--s-4);
}

/* Animated row rule: rows that sweep their hairline in (default = drawn) */
.sweep-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--rule-w);
  background: var(--hairline);
  transform-origin: left center;
}
.ledger-row.has-sweep {
  border-bottom: none;
}

/* ---------- Ruled table + mobile scroll frame ---------- */
.scroll-frame {
  position: relative;
}
.scroll-frame-hint {
  display: none;
}
@media (max-width: 749px) {
  .scroll-frame {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border: var(--rule-w) solid var(--hairline);
  }
  .scroll-frame > table,
  .scroll-frame > .table-min {
    min-width: 640px;
  }
  .scroll-frame-hint {
    display: block;
    font-family: var(--font-mono);
    font-size: var(--fs-13);
    text-transform: uppercase;
    letter-spacing: var(--track-label);
    color: var(--graphite);
    margin-bottom: var(--s-1);
  }
}

/* ---------- Footnotes ---------- */
.fn-mark {
  font-family: var(--font-mono);
  font-size: 0.7em;
  vertical-align: super;
  color: var(--orange-ink);
  text-decoration: none;
}
.footnotes {
  margin-top: var(--s-5);
  padding-top: var(--s-2);
  border-top: var(--rule-w) solid var(--hairline);
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  color: var(--graphite);
}
.footnotes li {
  list-style: none;
}
.footnotes .fn-no {
  color: var(--orange-ink);
  margin-right: 6px;
}

/* ---------- Form: numbered line items ---------- */
.form-rows {
  border-top: var(--rule-w) solid var(--hairline);
}
/* Inside the panel the top rule is redundant: the panel's own border is already
   the boundary, and 45px below it a second hairline read as a stray line rather
   than as the head of a ledger. Every field still closes on its own rule, so the
   run of rows is unbroken. */
.wwm-panel .form-rows {
  border-top: 0;
}
/* 16ch wrapped "02 COMPANY, AND ROUGHLY HOW MANY PEOPLE" to three ragged lines
   and "04 WHICH PROCESS ARE WE TALKING ABOUT?" to three more, which made the
   labels the noisiest thing in the panel. The labels were shortened first,
   because the copy was the actual problem; 20ch then holds every one of them on
   a single line. Keep these numbered: they are form fields, not sections, so
   the "do not number sections on a sales page" rule does not reach them. */
.field {
  display: grid;
  grid-template-columns: 20ch 1fr;
  align-items: start;
  gap: var(--s-2);
  padding: var(--s-3) 0 var(--s-2);
  border-bottom: var(--rule-w) solid var(--hairline-strong);
  transition: border-color var(--t-micro) var(--ease-micro);
}
.field:focus-within {
  border-bottom: 2px solid var(--orange-ink);
  padding-bottom: calc(var(--s-2) - 1px);
}
.field-label {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  color: var(--graphite);
  padding-top: 6px;
}
@media (max-width: 749px) {
  .field {
    grid-template-columns: 1fr;
    gap: 2px;
  }
  .field-error {
    grid-column: 1;
  }
}
.field input,
.field textarea {
  width: 100%;
  font-size: var(--fs-18);
  line-height: 1.4;
  padding: 2px 0 4px;
  min-height: 44px;
}
.field textarea {
  resize: vertical;
  min-height: 110px;
}
.field input:focus,
.field textarea:focus {
  outline: none; /* the field rule carries focus; :focus-visible covers keyboard nav elsewhere */
}
.field-error {
  grid-column: 2;
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  color: var(--orange-ink);
  display: none;
}
.field.is-invalid .field-error {
  display: block;
}
.field.is-invalid {
  border-bottom-color: var(--orange-ink);
}
.form-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  margin-top: var(--s-3);
  flex-wrap: wrap;
}
.form-status {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  color: var(--graphite);
}
.form-status.is-error {
  color: var(--orange-ink);
}
.received {
  display: none;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-3);
}
.received.is-on {
  display: inline-flex;
}
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* =====================================================================
   Work With Me layout  ·  contact page
   Rev. H.3: THE FORM IS A PANEL, and the panel is the fix for a real defect.

   .bg-grid is a fixed layer at z-index 0 and main sits above it at z-index 1,
   so page content already paints over the living grid. The form still showed
   the cursor patch straight through it because every element in it was
   transparent: the panel had no background, the inputs had no background, and
   a 1px hairline was the entire affordance. So the strongest structure
   anywhere near the pointer was the decorative quarter-cell sub-grid, drawn
   over the exact rectangle the reader was aiming at. And because the patch
   TRACKS the cursor, it was in the way every single time, by construction.
   Max: "the interactive cursor thing is making the action of filling the
   forms unintuitive."

   Why an opaque panel is allowed here when Rev. G made .sec--tint translucent
   for the opposite reason: the rule is not "never occlude the grid", it is
   THE GRID MAY STOP ONLY WHERE THE READER CAN SEE THE OBJECT THAT STOPPED IT.
   See the note above .sec--tint ("Inversion bands stay opaque on purpose")
   and scripts/measure-grid.mjs, which sanctions exactly this: "a .fig is an
   opaque mount sitting ON the paper, so the grid stops at its edge the way it
   stops at any drawn object." A full-bleed section with a barely-there wash
   gives the eye no such object, which is why THAT one read as broken.

   Consequence: the border and the shadow are LOAD-BEARING, not decoration.
   They are the whole reason the stop reads as occlusion rather than as a
   rendering fault. If the edge ever stops being legible, strengthen the edge,
   never the fill.

   Surface is --mount, which already exists as "near-white mount". It is
   deliberately quiet so the orange Book button stays the loudest thing on the
   page: the form is the escape hatch, not the outcome.

   The panel is ONE object spanning the row, and that is a fix, not a taste.
   The form and the contact details used to be two grid columns, 1/9 and 10/-1,
   and at 1440 that put a 619px form beside 208px of rail. Three arrangements
   were rendered and measured before this one:
     - rail top-aligned          411px of empty paper under it
     - rail stretched, space-between   the void moved into the MIDDLE of the
                                 column, 411px between two blocks that then read
                                 as unrelated fragments. Worse than the original
     - panel at 1/9, no rail     the void became a 450px column of nothing
                                 beside the densest object on the page
   None of those is fixable by tuning, because the mismatch is the content, not
   the alignment. One object filling the row removes it. THE PAIR RULE says to
   fix a height mismatch with content; deleting the second column is the most
   direct form of that.
   ===================================================================== */
.wwm-panel {
  margin-top: var(--s-5);
  background: var(--mount);
  border: var(--rule-w) solid var(--hairline);
  box-shadow: 0 14px 34px -26px rgba(20, 19, 17, 0.4);
  padding: var(--s-4);
}
/* The contact details, hanging under the form inside the same panel. They are
   here rather than in a rail because this is where a reader asks for them: "how
   else do I reach you" and "when do I hear back" are questions that arrive at
   the Send button, and an answer belongs where the question is asked. */
.wwm-side {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  gap: var(--s-4);
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: var(--rule-w) solid var(--hairline);
}
.wwm-side .label {
  color: var(--graphite);
  margin-bottom: var(--s-2);
}
.wwm-side ul {
  list-style: none;
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  line-height: 1.7;
}
.wwm-side li {
  padding: var(--s-1) 0;
}
@media (max-width: 749px) {
  .wwm-panel {
    padding: var(--s-3);
  }
  .wwm-side {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-3);
  }
}

/* ---------- Mini email capture (notify rows) ---------- */
.notify {
  display: flex;
  gap: var(--s-2);
  align-items: stretch;
  border-bottom: var(--rule-w) solid var(--hairline);
  max-width: 420px;
}
.notify input {
  flex: 1;
  min-width: 0;
  font-size: var(--fs-16);
  min-height: 44px;
}
.notify button {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  color: var(--orange-ink);
  min-height: 44px;
  padding-inline: var(--s-1);
  transition: color var(--t-micro) var(--ease-micro);
}
.notify button:hover {
  color: var(--werk-orange);
}

/* ---------- Title block (page identity, footer) ---------- */
.site-foot {
  margin-top: var(--sec-pad);
  position: relative;
  z-index: 1;
}
/* Rev. E: .title-block (the four boxed footer cells: Drawn by / Based in /
   Revision / Sheet) is deleted, not hidden. Max: "Remove the footer elements in
   the boxes, and remove those boxes as well." It was the drawing-register
   conceit applied to a footer and it told a buyer nothing. The foot line now
   closes the page on its own, so it carries the closing rule itself. */
.foot-line {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s-3);
  flex-wrap: wrap;
  padding: var(--s-4) 0;
  border-top: var(--rule-w) solid var(--hairline);
}

/* ---------- Guarantee / statement box ---------- */
/* Rev. F: two columns on a wide sheet, statement left and detail right. It was
   a stacked pair inside a box that spans the full 1325px, so the box drew a
   border around a large amount of nothing. Same rule as .lead-row: the column
   is the measure, and a statement gets a partner in its row. */
.guarantee {
  margin-top: var(--s-5);
  border: var(--rule-w) solid var(--ink);
  padding: var(--s-4);
  display: grid;
  gap: var(--s-2) var(--s-5);
  align-items: end;
}
@media (min-width: 900px) {
  .guarantee {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  }
}
.guarantee p {
  font-size: clamp(var(--fs-24), 2.6vw, var(--fs-32));
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  letter-spacing: -0.01em;
  line-height: 1.15;
  /* Rev. F: was 40ch, which rendered 493px of text inside a bordered box that
     spans the full 1325px sheet. Measured fill 0.37, and it looked worse than
     the number sounds because the empty part is INSIDE a drawn border. The box
     is the measure now; the text fills it. */
  max-width: min(62ch, 100%);
  text-wrap: balance;
}
.build-cta {
  margin-top: var(--s-4);
}
.work-stats {
  margin-top: var(--s-2);
  color: var(--graphite);
}

/* ---------- Placeholder sheets (unbuilt pages) ---------- */
/* Rev. F: the 52ch cap here shrank the bordered box itself to 476px, and then
   .guarantee p capped the text inside that to 217px. Two caps compounding
   produced the worst fill ratio measured anywhere on the site, 0.16: a 29
   character sentence wrapped onto two lines inside a small box in a 1325px
   sheet. The box now runs the sheet like every other .guarantee. */
.placeholder-note {
  margin-top: var(--s-3);
}

/* =====================================================================
   THE INVERSION BAND  ·  Rev. C
   Paper stays the page ground. These are the moments the sheet is
   allowed to be loud: the proof band and the closing ask. Three per
   page maximum, or the device stops meaning anything.
   Every colour pairing here is checked by scripts/check-contrast.mjs.
   ===================================================================== */
.band {
  position: relative;
}
/* THE SECTION-GAP RULE, calibrated in Rev. D.
   Rev. C2 stated it as "a full-bleed surface is preceded by --sec-gap of page
   ground, and its own padding is never a substitute". That fixed the hero CTA
   touching the black band, but it double-counted: 160px of ground plus 96px of
   internal padding put 256px of nothing above every band, and the same again
   below it. The render showed it as a hole, which is the opposite failure.
   The rule now reads: a full-bleed surface is preceded by --s-6 of page ground
   AND carries --s-6 of internal padding, and the section after it opens with
   --s-6 rather than the full gap, because the surface already paid half.
   Result: about 192px of separation everywhere, one rhythm down the sheet. */
.band,
.sec--invert,
.sec--tint {
  margin-top: var(--s-6);
  padding-block: var(--s-6);
}
.band + .sec,
.sec--invert + .sec,
.sec--tint + .sec {
  padding-top: var(--s-6);
}
.band--invert,
.sec--invert {
  background: var(--press-black);
  color: var(--on-dark);
}
.sec--invert {
  /* The gap before an inverted section must be paper, not ink, so the top
     padding moves off .sec and becomes a margin. Values above. */
  padding-top: var(--s-6);
}
.band--invert > .wrap,
.sec--invert > .wrap {
  position: relative;
}
/* The fixed .page-rules sit behind main, so a full-bleed dark band would
   otherwise cut the printed sheet in half. Redraw them on the band. */
.band--invert > .wrap::before,
.band--invert > .wrap::after,
.sec--invert > .wrap::before,
.sec--invert > .wrap::after {
  content: "";
  position: absolute;
  top: calc(var(--s-6) * -1);
  bottom: calc(var(--s-6) * -1);
  width: var(--rule-w);
  background: var(--on-dark-rule);
  pointer-events: none;
}
.band--invert > .wrap::before,
.sec--invert > .wrap::before {
  left: calc(var(--margin) - var(--rule-inset));
}
.band--invert > .wrap::after,
.sec--invert > .wrap::after {
  right: calc(var(--margin) - var(--rule-inset));
}

/* Component remapping inside a band. Same components, lifted colours. */
.band--invert .body-l,
.sec--invert .body-l,
.band--invert .reg-mark,
.sec--invert .reg-mark,
.band--invert .strip-cap,
.band--invert .work-stats,
.sec--invert .work-stats,
/* Rev. H.2. .cta-micro was inheriting --graphite, which is the SECONDARY TEXT
   token measured against paper, onto press-black: 2.46:1 on 13px type, an AA
   failure that had been live on the homepage closing band. It survived because
   neither gate could see it. check-contrast proves token PAIRINGS, and
   graphite-on-paper passes; it cannot know which token a component uses on
   which surface. And axe returned zero violations because it never got as far
   as an opinion: the living-grid pseudo-elements overlay the sections, so 112
   of 157 text nodes on this page come back "background could not be determined"
   and land in `incomplete`, which test-a11y.mjs did not report.
   scripts/check-contrast-rendered.mjs is the gate that closes it. */
.band--invert .cta-micro,
.sec--invert .cta-micro {
  color: var(--on-dark-dim);
}
.band--invert .tlink,
.sec--invert .tlink {
  color: var(--on-dark-orange);
}
.band--invert .tlink:hover,
.sec--invert .tlink:hover {
  color: var(--on-dark-orange);
  text-decoration-color: var(--on-dark);
}
.band--invert .ledger,
.sec--invert .ledger,
.band--invert .ledger-row,
.sec--invert .ledger-row {
  border-color: var(--on-dark-rule);
}
.band--invert .ledger-row::before,
.sec--invert .ledger-row::before {
  background: var(--on-dark-orange);
}
.band--invert .reg-line,
.sec--invert .reg-line {
  background: var(--on-dark);
}
.band--invert .chip--approved,
.sec--invert .chip--approved {
  color: var(--on-dark-approved);
}
.band--invert .chip--review,
.sec--invert .chip--review {
  color: var(--on-dark-review);
}
.band--invert .chip--neutral,
.sec--invert .chip--neutral {
  color: var(--on-dark);
}
.band--invert .cta-block,
.sec--invert .cta-block {
  background: var(--on-dark-orange);
  color: var(--ink);
}
.band--invert .cta-block:hover,
.sec--invert .cta-block:hover {
  background: var(--paper);
  color: var(--ink);
}
.band--invert .guarantee,
.sec--invert .guarantee {
  border-color: var(--on-dark-rule);
}
/* Rev. H put the four-test table on an inversion band and Rev. H.1 took it back
   off, at Max's word, after he saw it rendered. The remap is deleted rather than
   left dead, but the measured values are in HANDOFF.md so nobody has to derive
   them twice: on a dark band .crit needs --on-dark heads and pixel marks,
   --on-dark-dim for the tender column, and --blue-dim for the writing rules
   (4.29:1, non-text, needs 3:1). --blue itself is 2.3:1 on press-black and
   cannot be used there at all. */
.sec--invert :focus-visible {
  outline-color: var(--on-dark-orange);
}

/* =====================================================================
   OBJECTIONS  ·  native details, no JavaScript
   ===================================================================== */
/* Rev. D rebuild. Two failed attempts before this one:
     Rev. B  half-width answer      -> read as a cropped mistake
     Rev. C  columns: 2 multicol    -> ragged, unbalanced, unreadable order
   The defect in Rev. C is that CSS multicol fills column one to the bottom
   before starting column two, so a three-paragraph answer leaves a hole and
   the reader has no idea which way to read. Multicol is for continuous prose
   in print, not for two short paragraphs on screen.
   The fix is the pattern used on the client storefronts: an editorial Q and A
   ledger. The question holds the left column, the answer holds the right one,
   the pair spans the full sheet, and reading order is unambiguous. */
.faq {
  margin-top: var(--s-4);
  border-top: 2px solid var(--ink);
}
.faq details {
  border-bottom: var(--rule-w) solid var(--hairline);
}
.faq details[open] {
  background: var(--tint-sand);
}
/* Rev. E. The Rev. D fix above solved the multicol bug and introduced a worse
   one: a three-column row where the question was squeezed into 30ch and a mono
   teaser beside it gave the answer away. A reader who has already been told the
   answer has no reason to open the row, and a page of rows that all look
   answered reads as noise. This is the pattern the agency FAQs that work
   actually use, checked live against Capy's "Good questions.": one column, the
   question owns the whole row at a size you can read, a circled plus on the
   right says it opens, a hairline separates the rows, and the answer is behind
   the click. Nothing else in the row. */
.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-3);
  min-height: 76px;
  cursor: pointer;
  list-style: none;
  transition: background var(--t-micro) var(--ease-micro);
}
.faq summary:hover {
  background: var(--tint-sand);
}
.faq details[open] summary:hover {
  background: transparent;
}
.faq summary::-webkit-details-marker {
  display: none;
}
.faq-q {
  font-size: clamp(var(--fs-18), 1.5vw, var(--fs-24));
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  line-height: 1.25;
  max-width: 46ch;
}
/* The affordance. A bare glyph floating at the sheet edge had nothing anchoring
   it, so it read as a stray character rather than a control. */
.faq-mark {
  flex: none;
  width: 34px;
  height: 34px;
  border: var(--rule-w) solid var(--hairline-strong);
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: var(--fs-18);
  line-height: 1;
  color: var(--orange-ink);
  transition:
    border-color var(--t-micro) var(--ease-micro),
    background var(--t-micro) var(--ease-micro);
}
.faq summary:hover .faq-mark {
  border-color: var(--orange-ink);
}
.faq details[open] .faq-mark {
  border-color: var(--orange-ink);
  background: var(--paper);
}
.faq-mark::after {
  content: "+";
}
.faq details[open] .faq-mark::after {
  content: "\2212"; /* U+2212 minus sign, not a dash. The ban is about copy; this
                        is an affordance, and the correct glyph is free. */
}
/* The answer sits under its own question, at one readable measure. */
.faq-body {
  padding: 0 var(--s-3) var(--s-4) var(--s-3);
  color: var(--graphite);
  max-width: 78ch; /* an answer running the full 1300px sheet is unreadable */
}
.faq-body p + p {
  margin-top: var(--s-2);
}

/* =====================================================================
   FIT BLOCK  ·  the qualifier that replaced published pricing
   ===================================================================== */
.fit {
  margin-top: var(--s-5);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-5);
}
.fit-col {
  border-top: 2px solid var(--ink);
  padding-top: var(--s-2);
}
.fit-col--no {
  border-top-color: var(--hairline-strong);
}
.fit-h {
  color: var(--graphite);
}
.fit-col--yes .fit-h {
  color: var(--orange-ink);
}
/* Rev. E: the annotated plate.
   Two bulleted columns said the right thing and looked like a terms page, which
   is what Max called out. Each side is now a part drawing: a pixel portrait of
   the client in the middle, the three conditions called out around it on dashed
   leaders with a node at the text end.
   The two portraits are the same person on purpose. Only the chest pattern
   differs, and the pattern IS the argument: three even blocks for a process
   that repeats every week, the same blocks scattered for one that is different
   every month. Nobody is being made fun of. Maps in scripts/pixel-avatars.mjs. */
.fit-plate {
  position: relative;
  margin-top: var(--s-4);
  display: grid;
  grid-template-columns: minmax(0, 1fr) 128px minmax(0, 1fr);
  /* The row floor is what keeps the two portraits on the same line. Without it
     each plate sized its own rows from its own callout lengths, and the two
     figures sat 12px apart vertically, which is the first thing the eye sees.
     120px clears the longest callout (five lines) down to the 1280px collapse. */
  grid-template-rows: repeat(3, minmax(120px, auto));
  column-gap: var(--s-3);
  row-gap: var(--s-4);
  align-items: center;
}
/* The two spines the leaders terminate on, at the exact edges of the portrait
   column. Without them each leader stopped in mid-air and read as a stray tick
   rather than as a callout pointing at the figure. */
.fit-plate::before,
.fit-plate::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  border-left: var(--rule-w) dashed var(--hairline);
}
.fit-plate::before {
  left: calc(50% - 64px);
}
.fit-plate::after {
  left: calc(50% + 64px);
}
.av-plate {
  grid-column: 2;
  grid-row: 1 / span 3;
  align-self: center;
  display: grid;
  justify-items: center;
  gap: var(--s-2);
}
.av {
  display: block;
  width: 112px;
  height: auto; /* the map is 16 by 18, not square. Fixing both axes squashed it */
}
.fit-col--yes .av-body {
  fill: var(--ink);
}
.fit-col--yes .av-mark {
  fill: var(--werk-orange);
}
/* The non-fit portrait is drawn back, not drawn badly. It is the same figure in
   grey rather than ink and orange, because "probably not for you" is a routing
   decision and not a verdict on anybody.
   Two failed passes are worth recording, because both were only visible in the
   render. Graphite marks on a hairline-strong body sat about 1.9:1 apart and
   turned to mush at 112px, which killed the one thing the second portrait
   exists to show. Inverting it to paper marks on a graphite body fixed the
   contrast and broke the meaning: light cells inside a dark body read as HOLES,
   so at 375px the scattered pattern became a mouth full of teeth.
   A mark has to be darker than the body it sits on to read as a block rather
   than a gap. So: ink on hairline-strong, 5.4:1, dark marks on a light figure.
   The figure still recedes beside the ink-and-orange one because its mass is
   grey. Non-text marks, so the bar is 3:1. */
.fit-col--no .av-body {
  fill: var(--hairline-strong);
}
.fit-col--no .av-mark {
  fill: var(--ink);
}
.av-cap {
  font-size: var(--fs-13);
  line-height: 1.4;
  color: var(--graphite);
  text-align: center;
  max-width: 18ch;
}

/* The callouts. Position, not markup order, decides where each one lands, so
   the three sit around the portrait rather than under it. */
.cal {
  position: relative;
  font-size: var(--fs-16);
  line-height: 1.45;
  text-wrap: pretty; /* a right-aligned callout ending on "way." alone reads as a typo */
}
.cal--a {
  grid-column: 1;
  grid-row: 1;
  text-align: right;
}
.cal--b {
  grid-column: 3;
  grid-row: 2;
}
.cal--c {
  grid-column: 1;
  grid-row: 3;
  text-align: right;
}
.cal::after {
  content: "";
  position: absolute;
  top: 0.7em;
  width: var(--s-3);
  border-top: var(--rule-w) dashed var(--hairline-strong);
}
.cal::before {
  content: "";
  position: absolute;
  top: calc(0.7em - 2px);
  width: 5px;
  height: 5px;
  background: var(--orange-ink);
}
.fit-col--no .cal::before {
  background: var(--graphite);
}
.cal--a::after,
.cal--c::after {
  right: calc(-1 * var(--s-3));
}
.cal--a::before,
.cal--c::before {
  right: calc(-1 * var(--s-3));
}
.cal--b::after {
  left: calc(-1 * var(--s-3));
}
.cal--b::before {
  left: calc(-1 * var(--s-3));
}

/* Below this the side columns are too narrow to hold a sentence, so the plate
   stacks: portrait first, callouts as a plain annotated list under it. Two
   half-width plates on one row is the reason this breakpoint is so high. */
@media (max-width: 1279px) {
  .fit-plate {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
    row-gap: var(--s-3);
  }
  .fit-plate::before,
  .fit-plate::after {
    display: none;
  }
  .av-plate {
    grid-column: 1;
    grid-row: 1;
    justify-items: start;
  }
  .av-cap {
    text-align: left;
  }
  .cal {
    grid-column: 1;
    grid-row: auto;
    text-align: left;
    padding-left: var(--s-3);
  }
  .cal::after {
    display: none;
  }
  .cal::before {
    left: 0;
    right: auto;
    top: 0.6em;
  }
}

@media (max-width: 749px) {
  .fit {
    grid-template-columns: 1fr;
    gap: var(--s-4);
  }
}

/* =====================================================================
   BOOKING ROW  ·  contact sheet
   .proc-row aligns on the baseline, which a 56px CTA block overflows into
   the next row's rule. The booking row aligns to the start and gives the
   action its own column, so the button can never collide with the ledger.

   Rev. H.3: singular. The 30-minute scoping call was removed on Max's
   instruction and is now banned by scripts/lint-copy.mjs. With one row left
   the leading 8ch numeral column went too: "01" with nothing after it is a
   numbering system with one member, and it made a single call read as a menu
   the reader had to choose from. Two columns now, text and action.
   ===================================================================== */
.booking {
  margin-top: var(--s-5);
  border-top: var(--rule-w) solid var(--hairline);
}
.booking-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s-3);
  align-items: center;
  padding: var(--s-4) 0;
  border-bottom: var(--rule-w) solid var(--hairline);
}
.booking-h {
  font-size: var(--fs-24);
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  letter-spacing: -0.01em;
  line-height: 1.15;
}
.booking-line {
  margin-top: var(--s-1);
  color: var(--graphite);
  max-width: 56ch;
}
.booking-act {
  justify-self: end;
  align-self: center;
}

@media (max-width: 749px) {
  .booking-row {
    grid-template-columns: 1fr;
    gap: var(--s-2);
  }
  .booking-act {
    justify-self: start;
  }
}

/* =====================================================================
   HEADER  ·  Rev. C
   The Available chip is gone. It was a status that only ever said one
   thing, on a freelancer who is by definition available, so it carried
   no information and spent the most valuable pixel on the page. The
   header now ends on the only action the site wants.
   ===================================================================== */
.head-cta {
  display: inline-flex;
  align-items: center;
  min-height: 44px; /* the tap-target floor. 40 failed verify.mjs on the first pass */
  padding: 0 var(--s-3);
  background: var(--werk-orange);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--t-micro) var(--ease-micro);
}
.head-cta:hover {
  background: var(--ink);
  color: var(--paper);
}

/* =====================================================================
   SERVICE PILLARS  ·  what is actually being sold
   Four of them, straight off Max's business card. They appear twice:
   compact in the hero so the offer is legible in five seconds, and in
   full further down.
   ===================================================================== */
.pillars {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-3);
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: var(--rule-w) solid var(--hairline);
}
.pillar-ico {
  width: 22px;
  height: 22px;
  stroke: var(--ink);
  stroke-width: 1.5;
  fill: none;
  stroke-linecap: square;
}
.pillar-h {
  margin-top: var(--s-1);
  font-size: var(--fs-16);
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  line-height: 1.2;
}
.pillar-l {
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  line-height: 1.45;
  color: var(--graphite);
}

/* The full service blocks */
.svc {
  margin-top: var(--s-5);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-5);
}
.svc-item {
  border-top: 2px solid var(--ink);
  padding-top: var(--s-3);
  display: grid;
  gap: var(--s-2);
  align-content: start;
}
/* The .pain-fig device, on a light surface. Same geometry, different inks,
   because .pain-fig only ever runs inside an inversion band. Orange ink is
   5.4:1 on sand and graphite is 6.1:1, both already in check-contrast. */
.svc-fig {
  justify-self: start;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  max-width: 100%;
  border-left: 2px solid var(--werk-orange);
  padding: 4px 0 4px var(--s-2);
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  line-height: 1.35;
  color: var(--graphite);
}
.svc-fig b {
  font-size: var(--fs-24);
  font-weight: 400;
  color: var(--orange-ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  flex: none;
}
.svc-top {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
.svc-ico {
  width: 30px;
  height: 30px;
  stroke: var(--ink);
  stroke-width: 1.5;
  fill: none;
  stroke-linecap: square;
  flex: none;
}
.svc-h {
  font-size: var(--fs-24);
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  letter-spacing: -0.01em;
  line-height: 1.15;
}
.svc-line {
  color: var(--graphite);
  max-width: 46ch;
}
.svc-list {
  list-style: none;
  display: grid;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  line-height: 1.5;
}
.svc-list li {
  display: grid;
  grid-template-columns: 18px 1fr;
}
.svc-list li::before {
  content: "\2192";
  color: var(--orange-ink);
}

/* =====================================================================
   PAIN LIST  ·  lives inside the inversion band
   This replaced three numbers about Max. It is about the reader, and
   every figure in it is somebody else's published research, cited.
   ===================================================================== */
.pain {
  margin-top: var(--s-5);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-5) var(--s-4);
}
.pain-item {
  border-top: var(--rule-w) solid var(--on-dark-rule);
  padding-top: var(--s-3);
  display: grid;
  gap: var(--s-2);
  align-content: start;
}
/* Rev. D: six paragraphs in a row was six paragraphs in a row. Each pain now
   opens with a drawn mark, so the reader scans symbols first and reads second. */
.pain-ico {
  width: 34px;
  height: 34px;
  stroke: var(--on-dark-orange);
  stroke-width: 1.4;
  fill: none;
  stroke-linecap: square;
}
/* The figure chip. Present only where somebody else's research supplies a
   number. Three of six carry one; the other three carry none, on purpose. */
.pain-fig {
  justify-self: start;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  max-width: 100%;
  border-left: 2px solid var(--on-dark-orange);
  padding: 4px 0 4px var(--s-2);
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  line-height: 1.35;
  color: var(--on-dark-dim);
}
.pain-fig b {
  font-size: var(--fs-24);
  font-weight: 400;
  color: var(--on-dark-orange);
  font-variant-numeric: tabular-nums;
  white-space: nowrap; /* "1 to 4%" broke after "to" and read as two figures */
  flex: none;
}
.pain-h {
  font-size: clamp(var(--fs-18), 1.6vw, var(--fs-24));
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  line-height: 1.25;
  color: var(--on-dark);
  max-width: 30ch;
}
.pain-l {
  margin-top: var(--s-1);
  color: var(--on-dark-dim);
  font-size: var(--fs-16);
  line-height: 1.5;
  max-width: 42ch;
}
/* Rev. E: the band's conclusion, promoted out of the grid.
   It used to be the sixth cell, which put the one line that is NOT a specific
   example in the same box as the examples. Two of those examples (SKU
   photography, Shopify app load) went to the MayI Studio and Shopify sheets,
   where the reader who cares about them actually is. What is left is three
   examples and the point they add up to, so the point gets the full width. */
.pain-close {
  margin-top: var(--s-5);
  padding-top: var(--s-4);
  border-top: 2px solid var(--on-dark-orange);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: var(--s-5);
  row-gap: var(--s-2);
}
.pain-close .pain-ico {
  grid-column: 1;
  grid-row: 1;
}
.pain-close-h {
  grid-column: 1;
  grid-row: 2;
  font-size: clamp(var(--fs-24), 2.8vw, var(--fs-32));
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--on-dark);
  max-width: 22ch;
  text-wrap: balance;
}
.pain-close-l {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: end;
  color: var(--on-dark-dim);
  font-size: var(--fs-16);
  line-height: 1.5;
  max-width: 52ch;
}
.pain-src {
  margin-top: var(--s-5);
  padding-top: var(--s-3);
  border-top: var(--rule-w) solid var(--on-dark-rule);
  color: var(--on-dark-dim);
}

@media (max-width: 899px) {
  .pillars {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-3) var(--s-2);
  }
  .svc,
  .pain {
    grid-template-columns: 1fr;
    gap: var(--s-4);
  }
  .pain-close {
    grid-template-columns: minmax(0, 1fr);
  }
  .pain-close-h,
  .pain-close-l {
    grid-column: 1;
  }
  .pain-close-l {
    grid-row: auto;
    align-self: auto;
  }
}

/* =====================================================================
   SECTION SURFACES  ·  Rev. D
   The old system had one surface, so every section looked like the same
   section and the eye had nothing to hold on to. A tinted section is a
   full-bleed sand plate: quieter than an inversion band, louder than paper.
   Same section-gap rule applies. A tint never sits next to another tint.
   ===================================================================== */
/* Rev. G: translucent, not opaque, and the exact colour is unchanged.
   rgba(204, 191, 164, 0.2) composited over --paper #f4f1ea resolves to #ece7dc,
   which is --tint-sand to the byte. The reason for the change is the background
   grid: .bg-grid is a fixed layer at z-index 0 and main sits above it, so every
   opaque section was a hole the grid and its cursor patch fell into. On sheet
   03 that was three of eight sections, and the interaction switching itself off
   for a third of the page is a large part of why it read as broken rather than
   as subtle. The grid now runs continuously across paper and sand.
   Inversion bands stay opaque on purpose. Solid ink is still solid ink, and a
   grid stopping at a black band is a rule a reader can see. */
.sec--tint {
  background: rgba(204, 191, 164, 0.2);
}

/* =====================================================================
   PROCESS STEPPER  ·  Rev. D
   Five ledger rows were five ledger rows. This is a drawn route: numbered
   stops on a connector line, one open at a time, keyboard driven.
   No-JS and reduced-motion visitors get every panel open at once, which is
   the same information in a longer form. Nothing is hidden behind script.
   ===================================================================== */
.steps {
  margin-top: var(--s-5);
}
.steps-track {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--s-2);
  position: relative;
}
/* The route line, drawn per stop from this dot's centre to the next one's, so
   it stops at the last stop instead of running off the right margin. */
.step:not(:last-child)::before {
  content: "";
  position: absolute;
  top: 11px;
  left: 11px;
  width: calc(100% + var(--s-2));
  height: var(--rule-w);
  background: var(--blue-dim);
}
.step {
  position: relative;
  display: grid;
  gap: var(--s-1);
  align-content: start;
  justify-items: start;
  padding: 0 var(--s-2) var(--s-3) 0;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.step-dot {
  position: relative; /* sits above the route line it interrupts */
  z-index: 1;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: var(--rule-w) solid var(--blue);
  background: var(--paper);
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1;
  color: var(--blue);
  transition: background var(--t-stamp) var(--ease-stamp),
              color var(--t-stamp) var(--ease-stamp),
              transform var(--t-stamp) var(--ease-stamp);
}
.step[aria-selected="true"] .step-dot {
  background: var(--werk-orange);
  border-color: var(--werk-orange);
  color: var(--ink);
  transform: scale(1.18);
}
.step-name {
  font-size: var(--fs-16);
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  line-height: 1.2;
  color: var(--graphite);
}
.step[aria-selected="true"] .step-name {
  color: var(--ink);
}
.step-when {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  color: var(--blue);
}
.step:hover .step-name {
  color: var(--ink);
}
.step:hover .step-dot {
  background: var(--tint-sky);
}

.steps-panels {
  margin-top: var(--s-3);
}
.step-panel {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: var(--s-5);
  border-top: 2px solid var(--ink);
  padding-top: var(--s-3);
}
.step-panel + .step-panel {
  margin-top: var(--s-4);
}
/* Once the script has taken over, only the selected panel stays. */
.steps[data-live] .step-panel {
  display: none;
  margin-top: 0;
}
.steps[data-live] .step-panel[data-active] {
  display: grid;
}
.step-h {
  font-size: clamp(var(--fs-24), 2.4vw, var(--fs-32));
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  line-height: 1.1;
  letter-spacing: -0.01em;
}
.step-copy {
  margin-top: var(--s-2);
  color: var(--graphite);
  max-width: 52ch;
}
/* What the client physically holds at the end of the step. This is the part
   a process list never tells you, and it is the reason to keep reading. */
.step-out {
  background: var(--tint-sky);
  padding: var(--s-3);
  align-self: start;
  display: grid;
  gap: var(--s-1);
}
.step-out .label {
  color: var(--blue);
}
.step-out p {
  font-size: var(--fs-16);
  line-height: 1.5;
}
.step-out .mono {
  color: var(--graphite);
}

@media (max-width: 899px) {
  .steps-track {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .step:not(:last-child)::before {
    left: 11px;
    top: 24px;
    width: var(--rule-w);
    height: 100%;
  }
  .step {
    grid-template-columns: 22px minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--s-2);
    padding: var(--s-2) 0;
    min-height: 48px;
  }
  .step-panel {
    grid-template-columns: 1fr;
    gap: var(--s-3);
  }
}

/* =====================================================================
   FIGURE PLATE  ·  Rev. D
   The site sells imagery and showed none. There are no cleared photographs
   yet, and inventing one is off the table, so the plate carries a drawing
   instead: the actual thing being claimed, a pattern block with its seam
   allowance called out. A drawing is honest. A stock photo is not.

   Rev. E: the homepage's "Why me" section was cut on Max's note that the
   pattern-maker story does not connect for a reader who does not already know
   him, so nothing on index.html uses .plate today. The component is kept, not
   deleted: the three project sheets are still unbuilt and this is the figure
   component they are going to need. .why-grid, which had exactly one user, is
   gone with the section.
   ===================================================================== */
.plate {
  background: var(--tint-sky);
  padding: var(--s-4);
  display: grid;
  gap: var(--s-3);
  align-content: start;
}
.plate svg {
  width: 100%;
  max-width: 300px; /* uncapped, a 260x324 viewBox stretched the plate to ~700px tall */
  height: auto;
  display: block;
  margin-inline: auto;
}
.plate-cap {
  border-top: var(--rule-w) solid var(--blue-dim);
  padding-top: var(--s-2);
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  line-height: 1.5;
  color: var(--graphite);
}
.plate-cap b {
  color: var(--blue);
  font-weight: 400;
}

/* =====================================================================
   CASE-STUDY SHEET  ·  Rev. E
   Built for sheet 03 (LV2XL) and deliberately generic, because sheets 02
   and 04 are next and the pattern is the same: a head with a status strip,
   a numbered flow that teaches how the thing works, and a figure plate
   carrying the one artefact the whole page rests on.
   Everything here is composed from existing tokens. No new colours.
   ===================================================================== */
.sheet-back {
  margin-top: var(--s-3);
}
.sheet-deck {
  max-width: 58ch;
}
/* The status strip. Facts a reader can check without trusting the page:
   a live URL, a norm reference, a payment processor. Not adjectives. */
.sheet-meta {
  margin-top: var(--s-4);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-3);
  padding-top: var(--s-3);
  border-top: var(--rule-w) solid var(--hairline);
}
.sheet-meta .label {
  color: var(--graphite);
}

/* ---------- The numbered flow ---------- */
/* A static ordered list, not the homepage stepper. The stepper hides four
   panels behind a click, which is right when the reader is skimming five
   process stages and wrong when the sequence IS the explanation. */
.flow {
  margin-top: var(--s-5);
  list-style: none;
  display: grid;
  gap: var(--s-4);
  counter-reset: flow;
}
/* Three columns on a wide sheet: number, statement, detail. The first pass
   stacked the statement over the detail in one narrow column, which wrapped
   every heading onto two lines and left the right 45% of a 1324px sheet empty.
   That is the Rev. D measure bug again in a new component: the heading was
   wrapping because the column ran out, not because the writer wanted it to. */
.flow-step {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  column-gap: var(--s-3);
  row-gap: var(--s-2);
  padding-top: var(--s-3);
  border-top: var(--rule-w) solid var(--hairline);
}
/* Placement is explicit on purpose. With three children and a two-column track,
   auto-placement drops the body into row 2 of the NUMBER column, which is 64px
   wide and renders the paragraph one word per line. It passed the overflow gate
   and axe cleanly, and was only visible at 375 in a screenshot. */
.flow-no {
  grid-column: 1;
  grid-row: 1;
}
.flow-h,
.flow-body {
  grid-column: 2;
}
@media (min-width: 1000px) {
  .flow-step {
    grid-template-columns: 64px minmax(0, 0.85fr) minmax(0, 1.15fr);
    column-gap: var(--s-4);
    align-items: start;
  }
  .flow-h {
    grid-row: 1;
  }
  .flow-body {
    grid-column: 3;
    grid-row: 1;
  }
}
.flow-no {
  font-family: var(--font-mono);
  font-size: var(--fs-24);
  line-height: 1;
  color: var(--werk-orange);
  font-variant-numeric: tabular-nums;
}
.flow-h {
  font-size: clamp(var(--fs-18), 1.6vw, var(--fs-24));
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  line-height: 1.25;
  text-wrap: balance;
}
.flow-l {
  color: var(--graphite);
  max-width: 60ch;
}
.flow-l + .flow-out,
.flow-l + .flow-l {
  margin-top: var(--s-2);
}
.flow-out {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  line-height: 1.5;
  color: var(--blue);
  max-width: 60ch;
}

/* ---------- The route: the same four steps, drawn as a journey ---------- */
/* Rev. H. Max: "redesign it in a more interactive way where we are visually
   guided towards each point 1 to 4 like a little map. Make sure to add the
   motion design."

   A route, NOT a stepper. The note above still stands: the homepage stepper
   hides four panels behind a click, which is right when a reader is skimming
   and wrong here, because the sequence is the explanation. So every step stays
   on the page at every moment and the motion only adds the GUIDING: a rail
   traces down the sheet as the section scrolls, each stop lights as the trace
   reaches it, and the overview map above lights in the same order off the same
   value.

   The lit state is the resolved state. With no JavaScript, a failed CDN, or
   reduced motion, the rail is fully drawn and all four stops are lit, which is
   the site rule that CSS ships the finished document and motion only adds the
   journey. js/motion.js opts in by setting data-live, and only then does
   anything start dim.

   Nothing that carries meaning is ever dimmed: headings and body copy hold full
   ink in every frame. Only the node and the rail change, because a reader who
   scrolls past faster than the scrub resolves must still be able to read every
   word. */
.route {
  position: relative;
  margin-top: var(--s-5);
}
.flow--route {
  margin-top: 0;
  gap: var(--s-5);
}
/* baseline, not start. Rev. H.1, from Max: "fix the alignment of the text
   towards their respective numbers, it looks unaligned."
   The number sits vertically centred inside a 48px box, so its baseline lands
   about 32px below the box top, while a 23px heading's first baseline lands
   about 20px below its own top. Aligning the TOPS therefore printed the heading
   roughly 12px above the numeral it labels, and the row read as three columns
   that had each been positioned separately. Baseline alignment puts the
   numeral, the statement and the detail on one line of type, which is what the
   eye is looking for. */
.flow--route .flow-step {
  position: relative;
  border-top: 0;
  padding-top: 0;
  scroll-margin-top: 104px;
  align-items: baseline;
}
/* The number becomes the stop. 48px square on a 64px column, so the rail
   underneath lands on 23px at every width. */
.flow--route .flow-no {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  font-size: var(--fs-16);
  line-height: 1;
  color: var(--werk-orange);
  background: var(--paper);
  border: 2px solid var(--werk-orange);
  transition:
    color var(--t-stamp) var(--ease-stamp),
    border-color var(--t-stamp) var(--ease-stamp),
    background var(--t-stamp) var(--ease-stamp);
}
/* Step 03 is the one the heading names, so the route marks it rather than
   leaving the claim to the prose. */
.flow--route .flow-step--key .flow-no {
  background: var(--werk-orange);
  color: var(--ink);
}
.flow-link {
  position: absolute;
  left: 23px;
  top: 56px;
  bottom: calc(var(--s-5) * -1);
  width: 2px;
  background: var(--hairline);
}
.flow-link i {
  display: block;
  height: 100%;
  background: var(--werk-orange);
  transform: scaleY(var(--link-p, 1));
  transform-origin: top center;
}
/* Dim states exist only once motion.js has taken control. */
.route[data-live] .flow-step[data-reached="false"] .flow-no {
  color: var(--graphite);
  border-color: var(--hairline);
  background: var(--paper);
}

/* ---------- The overview map ---------- */
/* Four stops on one rail, above the route they summarise. It is the "little
   map": the whole sequence readable in one glance before any of it is read in
   detail, and every stop is an anchor into its step. */
.map-track {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-2);
  position: relative;
  padding-top: 22px;
}
/* The rail ends on the last node, not at the column edge. With four 1fr columns
   and one gap g, the last node's right edge sits at 75% + 0.75g + 14px, so the
   tail to trim is 25% - 0.75g - 14px. Written out rather than eyeballed, because
   a rail that overshoots its final stop reads as an unfinished route. */
.map-track::before,
.map-track::after {
  content: "";
  position: absolute;
  left: 0;
  right: calc(25% - var(--s-2) * 0.75 - 14px);
  top: 6px;
  height: 2px;
}
.map-track::before {
  background: var(--hairline);
}
.map-track::after {
  background: var(--werk-orange);
  transform: scaleX(var(--route-p, 1));
  transform-origin: left center;
}
.map-link {
  position: relative;
  display: grid;
  align-content: start;
  gap: 2px;
  min-height: 44px;
  padding-bottom: var(--s-1);
  text-decoration: none;
}
.map-node {
  position: absolute;
  top: -22px;
  left: 0;
  width: 14px;
  height: 14px;
  background: var(--paper);
  border: 2px solid var(--werk-orange);
  transition:
    border-color var(--t-stamp) var(--ease-stamp),
    background var(--t-stamp) var(--ease-stamp);
}
.map-stop--key .map-node {
  background: var(--werk-orange);
}
.map-link b {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  font-weight: 400;
  color: var(--orange-ink);
}
.map-name {
  font-size: var(--fs-18);
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  line-height: 1.2;
}
.map-link:hover .map-name {
  color: var(--orange-ink);
}
.map[data-live] .map-stop[data-reached="false"] .map-node {
  border-color: var(--hairline);
  background: var(--paper);
}
@media (max-width: 599px) {
  /* Four stops across 311px puts "Reconcile" on two lines at 8 characters. Two
     rows of two, and the rail goes with them rather than running behind a
     wrapped grid it no longer describes. */
  .map-track {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    row-gap: var(--s-3);
  }
  .map-track::before,
  .map-track::after {
    display: none;
  }
  .map-node {
    position: static;
    margin-bottom: var(--s-1);
  }
}

/* ---------- The reconciliation figure ---------- */
/* Built in HTML rather than SVG on purpose: it is two numbers and an equals
   sign, and as real text it stays selectable, scalable and translatable.
   SVG text would have bought nothing and cost all three. */
.recon {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--s-3);
}
.recon-side {
  display: grid;
  gap: var(--s-1);
}
.recon-op {
  font-family: var(--font-mono);
  font-size: var(--fs-32);
  line-height: 1;
  color: var(--blue-dim);
}
/* The whole figure is one token: "CHF 1'292'588.50" wrapping after "CHF" split
   the amount across two lines and destroyed the only thing the plate is for,
   which is two identical numbers read side by side. Capped at 24px, which is
   the largest size that holds 16 mono characters in half of a 720px plate. */
.recon-fig {
  font-family: var(--font-mono);
  font-size: clamp(var(--fs-18), 2vw, var(--fs-24));
  line-height: 1.1;
  white-space: nowrap;
  color: var(--blue);
  font-variant-numeric: tabular-nums;
}
.recon-src {
  color: var(--graphite);
}
.recon-delta {
  margin-top: var(--s-3);
  padding-top: var(--s-3);
  border-top: var(--rule-w) solid var(--blue-dim);
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
}
.recon-delta b {
  font-family: var(--font-mono);
  font-size: var(--fs-24);
  font-weight: 400;
  color: var(--orange-ink);
  font-variant-numeric: tabular-nums;
}
.recon-plate {
  max-width: 720px;
}

@media (max-width: 749px) {
  .flow-step {
    grid-template-columns: 40px minmax(0, 1fr);
    gap: var(--s-2);
  }
  .flow-no {
    font-size: var(--fs-18);
  }
  /* Stacked, the equals sign stops being an operator between two columns
     and starts reading as a stray character, so it goes. */
  .recon {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-3);
  }
  .recon-op {
    display: none;
  }
}

/* =====================================================================
   CASE-STUDY SHEET, Rev. F additions
   Max's standing complaint, now measured: "why do you always write text
   that ends at the middle of the screen?" A sweep of all five pages found
   ~20 real instances, and they traced to CSS caps, not to page markup.
   Two structural rules fix it, and both live here rather than in a page:

   RULE A. A section lead is a two-column ROW, not a stacked heading and
   intro. Inside a lead row the heading loses its ch cap, because the
   column is now the measure. This is the Rev. D rule enforced by
   structure: a heading wraps because a designed column ended, never
   because a ch cap ran out mid-sheet.

   RULE B. A prose block always has a partner in its row. Every figure
   below exists to be that partner as much as to teach.

   Both are gated by scripts/check-rows.mjs, which fails the build on any
   text block filling under 60% of its row with nothing beside it.
   ===================================================================== */
/* Rev. G. Max, on the "What transfers" lead: "There is a random text on top of
   the image, breaking the balance between the section... What is even that text
   doing there? Is that a title? the paragraph following?"

   TWO ATTEMPTS. The first one is recorded because it was the wrong kind of fix
   and the difference matters.

   Attempt 1 kept the two-column lead and treated the symptom. The row was
   `align-items: end`, which bottom-aligned the dek against a heading of
   arbitrary height; measured at 1440, the gap from the top of a heading to the
   top of its own dek ran 201, 91, 42, 42, 14, 14, 14px, so on a tall heading the
   dek drifted until it sat nearer the figure BELOW it than the heading it
   belongs to. Top-aligning it and hanging it off a hairline took every gap to
   0px, passed every gate, and Max looked at the result in a browser and said the
   main thing still was not fixed. He was right. A two-line paragraph alone in a
   half-column reads as an orphan at ANY vertical position, and the hairline made
   it worse by giving it a second unexplained start. Aligning an orphan does not
   stop it being an orphan.

   Attempt 2, this one, answers his actual question. A dek is the paragraph
   following the title, so it goes DIRECTLY UNDER the title, in the same column,
   and the question cannot be asked. The heading gets the whole sheet instead of
   6/11 of it, which is a bonus rather than a cost: measured heading fill went
   from roughly 0.55 to 0.76 through 0.98, and "The method transfers. The
   construction knowledge does not." now lands on one line instead of three.
   Roughly 200px comes off each of these sections as well, because the void that
   used to sit beside the dek is gone.

   Why check-rows exempts the dek rather than the measure being widened to pass:
   at 70ch the fill was still only 0.55 to 0.60 and the line ran 84 characters,
   which trades a real readability rule for a gate that is asking the wrong
   question. A standfirst is not a paragraph alone on a sheet. It is the second
   half of a block whose first half fills the sheet, and that is what the eye
   reads. Exemption written into scripts/check-rows.mjs. */
.lead-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}
/* A second lead further down the same section needs air above it. This is a
   class and not an inline style on purpose: spacing above a row belongs to the
   row, and an inline `style` is invisible to every gate we own, which is half of
   why one spacing defect survived on three pages in Rev. G. */
.lead-row--gap {
  margin-top: var(--s-6);
}
.lead-row .sec-h,
.lead-row .sec-h--lead {
  max-width: 100%;
}
.lead-row .sec-intro {
  margin-top: var(--s-3);
  max-width: 66ch;
  line-height: 1.5;
}
.band--invert .lead-row .sec-intro,
.sec--invert .lead-row .sec-intro {
  color: var(--on-dark-dim);
}

/* Prose left, figure right (or the reverse with --flip). The workhorse row. */
.duo {
  margin-top: var(--s-5);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: var(--s-5);
  align-items: start;
}
.duo--wide-l {
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
}
.duo--wide-r {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
}
/* Both columns settle onto one shared bottom line. Only correct where the
   partner is a solid plate rather than loose text: a plate cannot be mistaken
   for a stray paragraph, which is the failure that made .lead-row's bottom
   alignment wrong. Named rather than inline so scripts/check-drift.mjs can
   exempt it deliberately. */
.duo--baseline {
  align-items: end;
}
/* THE PAIR RULE, Rev. H, and it comes straight out of Max's review: the hero row
   is "perfect, the image is aligned with the title and paragraph", and no other
   row on the sheet held that standard.
   The defect is NOT the top edge. Every .duo already starts both columns on the
   same line and check-drift.mjs proves it. The defect is the bottom: the hero's
   two columns both end at the same y, and everywhere else the figure outran its
   prose by 217 to 339px, leaving a column of empty paper under the text. A void
   under a text block reads exactly like the mid-sheet ending check-rows exists
   to catch, one axis over.
   It cannot be fixed by capping the figure. These drawings carry 11px labels in
   a 440 viewBox, so shrinking the plate to a 181px column takes the labels to
   under 6px, which is the Rev. G lesson about svg text all over again. So the
   fix is content, not scale: the column stretches to the row, its prose sits at
   the top, and its LAST child hangs at the foot, level with the figure's
   caption. That is how a note sits at the foot of a printed column, and it is
   only correct when the hanging element genuinely belongs at the end (a source
   note, a closing statement), never as a spacer.
   Two children exactly. More than two and space-between starts spreading the
   prose itself, which is the failure this is meant to avoid. */
.duo--foot {
  align-items: stretch;
}
.duo--foot > :first-child {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--s-5);
}
/* The hanging element keeps its rule and its padding and loses its own top
   margin: separation in a flex column is the gap's job, and a margin on top of
   it double-counts (the Rev. D spacing lesson, one component down). */
.duo--foot > :first-child > * {
  margin-top: 0;
}
@media (max-width: 899px) {
  /* Stacked, there is no row to end level with, so the foot element goes back to
     sitting directly under the prose. */
  .duo--foot > :first-child {
    display: block;
  }
  .duo--foot > :first-child > * + * {
    margin-top: var(--s-4);
  }
}
.duo p + p {
  margin-top: var(--s-3);
}

@media (max-width: 899px) {
  .lead-row,
  .duo {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--s-3);
  }
  .duo {
    row-gap: var(--s-4);
  }
}

/* ---------- Figures ---------- */
/* All drawn, all teaching, zero photography. A figure earns its place by
   explaining something the prose beside it would otherwise have to spend a
   paragraph on. */
.fig {
  background: var(--tint-sky);
  padding: var(--s-4);
  display: grid;
  gap: var(--s-3);
  align-content: start;
}
.fig svg {
  width: 100%;
  height: auto;
  display: block;
}
.fig-cap {
  border-top: var(--rule-w) solid var(--blue-dim);
  padding-top: var(--s-2);
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  line-height: 1.5;
  color: var(--graphite);
}
.fig-cap b {
  color: var(--blue);
  font-weight: 400;
}
/* Drawing primitives, so every figure on the site shares one hand. */
.d-rule {
  stroke: var(--blue-dim);
  stroke-width: 1.2;
  fill: none;
}
.d-ink {
  stroke: var(--blue);
  stroke-width: 1.6;
  fill: none;
}
.d-hot {
  stroke: var(--orange-ink);
  stroke-width: 1.6;
  fill: none;
}
.d-fill-hot {
  fill: var(--werk-orange);
  stroke: none;
}
.d-fill-soft {
  fill: var(--mount);
  stroke: var(--blue-dim);
  stroke-width: 1.1;
}
.d-dash {
  stroke: var(--blue-dim);
  stroke-width: 1.1;
  stroke-dasharray: 5 4;
  fill: none;
}
.d-label {
  font-family: var(--font-mono);
  font-size: 11px;
  fill: var(--blue);
}
.d-label--hot {
  fill: var(--orange-ink);
}
.d-label--dim {
  fill: var(--graphite);
}
/* Rev. G. Every other figure on the sheet sits in a half-width column, where an
   11px label in a 440 viewBox renders at about 15px. The time field is full
   bleed, so the same 11px would render at 12.7px and the two numbers the figure
   exists for would be its smallest type. */
.d-label--lg {
  font-size: 15px;
}
/* ---------- The run clock (twenty hours becomes forty seconds) ---------- */
/* Rev. H, and it replaces a 1,200-cell field that stood about 800px tall. Max:
   "20h -> 40s design is too big for such a little information. Maybe try to have
   an animation where we see the amount of hours needed and the timer animation
   going from 20h to 40s, and a design spec that makes the 2 values correlate."

   THE CORRELATION IS THE CONSTRUCTION, not a garnish on it. One value t drives
   both marks: the readout prints t as a clock, and the bar's width is
   t / 72,000 seconds. They are two renderings of the same number, so they can
   never disagree, and the reader watches a full bar drain to a hairline while
   the digits fall. That is the ratio the old field needed 1,200 squares to say.

   The sliver is held at a 3px floor. True scale is 0.056 percent, which is
   0.7px on a 1,325px sheet and would not survive antialiasing. The caption says
   so, the same way the old figure said its corner cell was drawn full to be
   findable at all. An honest figure states where it stopped being to scale. */
.runfig {
  margin-top: var(--s-5);
}
.run {
  display: grid;
  gap: var(--s-3);
}
/* Two clocks, one document. The left one is frozen at the manual figure, the
   right one starts on the same value and runs down to forty seconds. They are
   equal for exactly one frame, which is the honest starting position and the
   reason the figure needs no arrow-shaped explanation of what is being
   compared. */
.run-pair {
  display: grid;
  grid-template-columns: auto minmax(var(--s-5), 1fr) auto;
  align-items: end;
  gap: var(--s-4);
}
.run-side {
  display: grid;
  gap: var(--s-1);
}
.run-side .label {
  color: var(--graphite);
}
.run-side--to {
  text-align: right;
}
.run-side--to .label {
  color: var(--orange-ink);
}
/* Tabular by way of .numeral, so a falling clock never reflows its own width and
   the two never trade places on the sheet. */
.run-clock {
  font-size: clamp(var(--fs-32), 4.6vw, var(--fs-64));
  line-height: 0.92;
  color: var(--ink);
  font-variation-settings: "wdth" 115;
}
/* The superseded value keeps full legibility and loses the ink: never invert the
   resolved state, just let the number that no longer applies recede. (This used
   to cite [data-swap] on the homepage as the same logic. That primitive never
   had a consumer and was deleted on 2026-07-29, so the rule now stands here on
   its own rather than pointing at something that no longer exists.) */
.run-clock--from {
  color: var(--graphite);
}
/* Becomes. Drawn in CSS rather than as a third svg on the sheet. */
.run-arrow {
  position: relative;
  align-self: center;
  height: var(--rule-w);
  background: var(--blue-dim);
  margin-bottom: 22px;
}
.run-arrow::after {
  content: "";
  position: absolute;
  right: 0;
  top: -5px;
  width: 10px;
  height: 10px;
  border-top: var(--rule-w) solid var(--blue-dim);
  border-right: var(--rule-w) solid var(--blue-dim);
  transform: rotate(45deg);
}
.run-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--s-1) var(--s-3);
}
/* The empty track is the site's drawing language, not a chart: a mount fill
   inside a blue hairline, the same pair as .d-fill-soft. */
.run-track {
  position: relative;
  height: 28px;
  background: var(--mount);
  border: var(--rule-w) solid var(--blue-dim);
}
/* The bar drains from the right, so whatever is still filled is the time still
   to run. Animated on WIDTH rather than on scaleX, deliberately: scaleX would
   take the resolved 0.056 percent to 0.7px on a 1,325px sheet and no min-width
   can clamp a transform. On width, min-width can, and does. */
.run-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 0.056%;
  min-width: 3px;
  background: var(--werk-orange);
}
.run-scale {
  color: var(--graphite);
}
.run-scale--to {
  text-align: right;
  color: var(--orange-ink);
}
@media (max-width: 899px) {
  /* Two 8-character clocks side by side need about 640px. Below that they stack
     and the arrow goes with them: a rule pointing right between two stacked
     values would be pointing at nothing. */
  .run-pair {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-3);
  }
  .run-arrow {
    display: none;
  }
  .run-side--to {
    text-align: left;
    padding-top: var(--s-2);
    border-top: var(--rule-w) solid var(--blue-dim);
  }
  .run-foot {
    flex-direction: column;
  }
  .run-scale--to {
    text-align: left;
  }
}

/* ---------- The primer box (the niche, stated once, in its own language) ---------- */
.primer {
  border-left: 2px solid var(--blue);
  padding: var(--s-1) 0 var(--s-1) var(--s-3);
}
.primer p + p {
  margin-top: var(--s-2);
}

/* ---------- Full-width closing statement on an orange rule ---------- */
/* The .pain-close pattern, on paper rather than in an inversion band. The
   conclusion of a section is not one of its examples, so it gets the sheet. */
.close-line {
  margin-top: var(--s-5);
  padding-top: var(--s-4);
  border-top: 2px solid var(--werk-orange);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: var(--s-5);
  row-gap: var(--s-2);
}
.close-h {
  font-size: clamp(var(--fs-24), 2.6vw, var(--fs-32));
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  line-height: 1.15;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
.close-l {
  color: var(--graphite);
  align-self: end;
}
/* Rev. H: the same device inside a .duo--foot column, where a two-column split
   of a 666px column would set a 37px statement to 16 characters a line. */
.close-line--stack {
  grid-template-columns: minmax(0, 1fr);
  /* Declared, not inherited. The stack variant is the only arrangement where
     the two lines sit one above the other, so the vertical gap between a 32px
     statement and the line under it is a typographic decision and belongs on
     screen here, rather than emerging from whatever the grid's gap happened to
     be for the two-column case. */
  row-gap: var(--s-3);
}
.close-line--stack .close-l {
  align-self: start;
}
/* A .close-line nested inside a .duo was picking up `.duo p + p`, so its two
   lines were separated by the grid's row-gap AND a 24px margin, and the
   component rendered 40px where it declares 24. Measured on the sheet 02 render,
   then found again unfixed on sheet 03, where it has shipped since Rev. H.
   This is the third instance of this exact leak in this stylesheet:
   `.cta-side .body-l` carries margin-top: 0 for the same reason, and the note
   there records that two columns off one rule starting 24px apart is the pair
   rule failing at 24px instead of at 300.
   So the fix is scoped to the COMPONENT and not to the duo. A component owns
   the spacing between its own parts, wherever somebody nests it, and a rule
   written the other way round only fixes the nesting that has been tried so
   far. */
.close-line > p + p {
  margin-top: 0;
}

/* ---------- The automation screen (four tests) ---------- */
/* Three columns: the test, how it answered for the tender job, and a blank
   column for the reader's own process. The blank column is the point: it
   invites the reader to disqualify themselves, which is the cheapest
   credibility a solo operator has and it filters the calendar. */
.crit {
  margin-top: var(--s-5);
  border-top: 2px solid var(--ink);
}
.crit-head,
.crit-row {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.2fr) minmax(0, 1fr);
  column-gap: var(--s-4);
  padding: var(--s-3) 0;
  border-bottom: var(--rule-w) solid var(--hairline);
}
.crit-head {
  padding-block: var(--s-2);
}
.crit-head span {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  color: var(--graphite);
}
.crit-head span:last-child {
  color: var(--blue);
}
/* Rev. G: the third head carries its own explanation. It used to sit in a mono
   caption below the table, which is 40px and one whole table away from the
   moment the question is asked, and Max read the whole table without finding
   it. The rule this enforces: an objection is answered where it is raised. */
.crit-head-you b {
  display: block;
  margin-top: 8px;
  max-width: 30ch;
  font-weight: inherit;
  text-transform: none;
  letter-spacing: 0;
  line-height: 1.5;
  color: var(--graphite);
}
.crit-test {
  font-size: var(--fs-18);
  font-weight: 560;
  font-variation-settings: "wdth" 105;
  line-height: 1.3;
  display: flex;
  align-items: flex-start;
  gap: var(--s-2);
}
/* One pixel mark per test, so the set of four is recognisable before it is
   read. Same idiom and same ink as the Fit portraits on the homepage, and
   generated the same way, from the ASCII maps in scripts/pixel-marks.mjs. */
.crit-mark {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  margin-top: 0;
}
.cm-body {
  fill: var(--ink);
}
.cm-accent {
  fill: var(--werk-orange);
}
.crit-here {
  color: var(--graphite);
  font-size: var(--fs-16);
  line-height: 1.5;
}
/* Three hairline writing rules, drawn not typed, so the column reads as a
   form waiting to be filled in rather than as missing content. */
.crit-you {
  display: grid;
  gap: 14px;
  align-content: start;
  padding-top: 6px;
}
.crit-you span {
  display: block;
  height: var(--rule-w);
  background: var(--blue-dim);
}
.crit-you span:last-child {
  width: 60%;
}
.crit-not {
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: 2px solid var(--werk-orange);
  max-width: 90ch;
}

/* Rev. H: the .xfer component (the four-blocks-against-one transfers split) is
   deleted with the "What transfers" section it was built for. Max removed that
   section and "The proof" in the Rev. H review. If either comes back, both the
   markup and this component are in git at 0a9f772 onward. */

@media (max-width: 899px) {
  .crit-head {
    display: none;
  }
  .crit-row {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--s-2);
  }
  .crit-you {
    display: none; /* blank rules with no header read as a rendering fault */
  }
  .close-line {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ---------- The reconciliation, second instance at small scale ---------- */
.recon--mini .recon-fig {
  font-size: var(--fs-18);
}
.recon--mini .recon-delta b {
  font-size: var(--fs-18);
}

/* ---------- What the fifteen minutes contains ---------- */
/* Sits beside the closing CTA so the final band does not end mid-sheet, and
   does a second job: it makes the click feel small by saying what is in it. */
.cta-side {
  border-top: var(--rule-w) solid var(--on-dark-rule);
  padding-top: var(--s-3);
  display: grid;
  gap: var(--s-2);
  align-content: start;
}
.cta-side .label {
  color: var(--on-dark-orange);
}
.cta-side ol {
  list-style: none;
  display: grid;
  gap: var(--s-2);
}
.cta-side li {
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr);
  gap: var(--s-2);
  color: var(--on-dark);
  max-width: 46ch;
}
.cta-side li b {
  font-family: var(--font-mono);
  font-weight: 400;
  color: var(--on-dark-dim);
}
.cta-side .mono {
  color: var(--on-dark-dim);
  margin-top: var(--s-1);
}
/* margin-top: 0 is load-bearing. This paragraph follows the .label, which is
   also a <p>, so `.duo p + p { margin-top: var(--s-3) }` was firing on it and
   pushing the left column's first line 24px below the right column's. Two
   columns hanging off one rule with their content starting on different lines
   is the pair rule failing at 24px instead of at 300. */
.cta-side .body-l {
  margin-top: 0;
  color: var(--on-dark-dim);
  max-width: 46ch;
}
/* Rev. H.1. Max: "have the What is in the fifteen minutes block sit next to the
   Fifteen minutes... block and align their height. The CTA button comes below
   them."
   Two .cta-side columns, so both hang off the SAME rule and both are the height
   of the row rather than one being a paragraph and the other a list. Stretch is
   what does the aligning: without it the shorter column ends 130px above the
   taller one and the pair reads as two unrelated blocks that happen to be
   side by side. The rule above each is what makes the equal height legible,
   because a rule gives the eye a top edge to measure the bottom against. */
.cta-pair {
  align-items: stretch;
}
/* Scoped to the pair, not to .build-cta globally: that class also carries the
   homepage and mayi-studio buttons, and a bare .build-cta rule down here would
   silently reflow both. */
.cta-pair + .build-cta {
  margin-top: var(--s-5);
}
/* Rev. H.2, when the homepage adopted this component. The fallback line sits
   under the button, so the reading order of the whole band is promise, then
   contents, then action, then the way out for somebody not ready to act.
   Written as a rule rather than the inline `style="margin-top: var(--s-3)"`
   it replaces: an inline margin on a child is invisible to check-drift and to
   check-rows, which is the second half of why the Rev. G heading defect
   survived on three pages. Spacing belongs in the stylesheet where a gate can
   read it. */
.build-cta + .cta-micro {
  margin-top: var(--s-3);
}

/* =====================================================================
   PHOTOGRAPHIC PLATES  ·  Rev. H.5, sheet 02
   The first images this site has ever carried. Everything above this
   block is a drawing, and the difference matters for two rules that were
   already in force here and had only ever applied to SVG.
   1. "A .fig is an opaque mount sitting ON the paper, so the living grid
      stops at its edge the way it stops at any drawn object." A
      photograph is more opaque than any drawing, so the doctrine holds,
      but only if its edge is VISIBLE: two of these plates are studio
      shots on pure white, and white on warm paper is a boundary the eye
      can miss. Every plate therefore carries a hairline. The grid may
      stop only where the reader can see the object that stopped it.
   2. The mount itself goes. --tint-sky is a diagram surface and it casts
      a blue over a photograph that a line drawing never has to survive.
      A photograph is its own mount.
   ===================================================================== */
.fig--photo {
  background: none;
  padding: 0;
}
/* The plate. The ratio is enforced here as well as in the file, so a
   mis-cropped asset cannot silently change the height of a row: the
   pictures are pre-cut to 2:3 and 1:1 by scripts/build-images.mjs and
   `cover` is the belt, not the trousers. It is also what holds the
   layout still while an image is still arriving, which is the whole
   reason width and height are on every <img> as well. */
.shot {
  position: relative;
  display: block;
  aspect-ratio: 2 / 3;
  overflow: hidden;
  border: var(--rule-w) solid var(--hairline);
  background: var(--mount);
}
.shot--sq {
  aspect-ratio: 1 / 1;
}
.shot img,
.shot video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The hero pair: the photograph, and one of the images made from it. Two
   plates in one figure so the argument of the page is complete inside the
   first screen, before a word of it has been read. */
.shots {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-2);
}

/* ---------- The four outputs ---------- */
.outs {
  margin-top: var(--s-5);
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-4) var(--s-3);
}
.out {
  display: grid;
  gap: var(--s-2);
  align-content: start;
}
.out-h {
  margin-top: var(--s-1);
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  color: var(--ink);
}
.out-l {
  color: var(--graphite);
  font-size: var(--fs-16);
  line-height: 1.5;
}
/* The video cell. autoplay + muted + loop + playsinline, no controls, no
   audio track at all: there is nothing to caption and nothing that can
   ambush a reader with sound. A browser that declines to autoplay shows
   the poster instead, which is honest degradation, and the copy in the
   cell describes the clip rather than claiming it is playing.
   Reduced motion is handled without a line of JavaScript, and it has to
   be: no CSS can un-set an autoplay attribute, so the still is authored
   as a real element and these two rules swap them.
   ⚠ THIS SWAP DOES NOT STOP THE DOWNLOAD, and the first version of this
   comment claimed it did. Measured: with the <video> at display:none a
   reduced-motion reader still fetched all 366 KB, because autoplay begins
   resource selection whatever the element's display is. What stops it is
   a `media` query on the <source> in the markup. Keep both: the media
   query makes the clip free, and these rules are what put the still on
   screen in its place. */
.shot-still {
  display: none;
}
@media (prefers-reduced-motion: reduce) {
  .shot-still {
    display: block;
  }
  .shot-play {
    display: none;
  }
}

/* ---------- The traceability strip ---------- */
/* Four square crops of the same region of the same garment, one per
   output. This is the page's proof and it is the only kind this site
   accepts in a first screen: something the reader checks with their own
   eyes instead of trusting the seller. Square, so it reads as a detail
   rather than as a smaller copy of the plate above it. */
.same {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-3);
}
.same-item {
  display: grid;
  gap: var(--s-2);
  align-content: start;
}
.same-cap {
  color: var(--graphite);
}

/* ---------- A source or provenance note on PAPER ---------- */
/* .pain-src is the same device inside an inversion band, and sheet 03
   reaches it on paper by overriding two colours with an inline style.
   Inline styling is what this repo has repeatedly been bitten by, so the
   paper variant is a class. */
.src-note {
  margin-top: var(--s-5);
  padding-top: var(--s-3);
  border-top: var(--rule-w) solid var(--hairline);
  color: var(--graphite);
}
/* Inside a .duo--foot column the gap does the separating and the rule
   does the marking, so the margin would double-count. Same lesson as
   .duo--foot > :first-child > *, one component along. */
.duo--foot .src-note {
  margin-top: 0;
}
/* The disqualifier, sitting beside the claim it qualifies rather than
   under it. Flush, because a .close-line normally opens a new movement
   under a section and this one is a column of a row: 64px of margin
   would start the two columns of that row on different lines, which is
   the pair rule failing at 64px instead of at 300. */
.close-line--flush {
  margin-top: 0;
}

@media (max-width: 1099px) {
  .outs,
  .same {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (max-width: 560px) {
  /* One column, and the page gets taller for it. That is the right trade
     on the one page whose product IS the pictures: two-up at 375 renders
     a 155px plate, and nobody buys imagery from a thumbnail. The strip
     below stays two-up, because its crops are tight enough to read
     small and its job is comparison, which wants the four in view at
     once. */
  .outs {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* A prose measure, as a class rather than as the inline style="max-width: 52ch"
   that sheet 03 uses in four places. Same value, same job, one difference that
   matters: a class is visible to check-rows and check-drift, and an inline
   style is not. Every spacing and measure defect that has survived more than
   one revision in this repo survived inside an attribute no gate could read. */
.measure {
  max-width: 52ch;
}
/* The traceability strip is a figure that IS its own row, rather than the half
   of a .duo every other figure on this site sits in, so its top spacing belongs
   to it. On the row, never on a child. */
.same-fig {
  margin-top: var(--s-5);
}

/* =====================================================================
   Consent banner
   ===================================================================== */
/* The analytics opt-in. Markup lives in partials/head.html, third in the
   document so the keyboard and screen readers meet the choice before the page
   argues; this rule is what moves it to the foot of the viewport.

   OPAQUE SURFACE, NOT A TINT. --mount rather than a translucent --paper,
   because the living grid and the cursor patch both paint behind everything on
   this site. check-contrast-rendered.mjs grades every run of text against the
   surface it ACTUALLY paints on, and a semi-transparent banner would put body
   copy over a moving field and fail it at some scroll positions and not
   others. An opaque panel makes the ratio a constant.

   The top hairline is the same rule the footer closes on, so the banner reads
   as another sheet edge rather than as an overlay from somewhere else. */
.consent {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  background: var(--mount);
  border-top: var(--rule-w) solid var(--ink);
}
.consent[hidden] {
  display: none;
}
/* The flex row sits INSIDE .wrap rather than on it. See the comment on the
   markup in partials/head.html: check-rows.mjs stops its ancestor walk at the
   .wrap, so a flex row declared on the .wrap itself is invisible to it and the
   copy is reported as unpartnered on every page. */
.consent-inner {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
  padding-block: var(--s-3);
}
/* 78ch, not the 68ch body measure. Two lines on a wide sheet rather than three:
   the banner covers the page while it is open, so its own height is a cost
   paid by the reader. */
.consent-copy {
  max-width: 78ch;
}
.consent-copy .label {
  color: var(--graphite);
  margin-bottom: var(--s-1);
}
.consent-copy .body-l {
  font-size: var(--fs-16);
}

/* Accept and Decline: same box, same weight, same row, no visual hierarchy
   between them. This is a legal requirement rather than a style preference.
   Consent has to be freely given, and a bright Accept beside a grey Decline is
   the defect German authorities enforce most often. The pair sits in one row
   so neither can be reached faster than the other. */
.consent-acts {
  display: flex;
  gap: var(--s-2);
  flex-shrink: 0;
}
.consent-btn {
  min-height: 48px;
  min-width: 120px;
  padding: 0 var(--s-3);
  border: var(--rule-w) solid var(--ink);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--fs-16);
  font-weight: 500;
  cursor: pointer;
  transition:
    background var(--t-micro) var(--ease-micro),
    color var(--t-micro) var(--ease-micro);
}
.consent-btn:hover {
  background: var(--ink);
  color: var(--paper);
}

/* The footer withdrawal control. It is a <button> because it opens something
   here rather than navigating, but it sits inline among links, so it has to
   shed every default a button carries and inherit .tlink's underline. */
/* The footer link row. It wraps at narrow widths, and the row-gap is sized
   against .tap's geometry rather than picked for looks. .tap buys its 44px hit
   area with `padding-block: 14px; margin-block: -14px`, so a target's BOX is
   48px tall while its line advance is only about 19px. Measured at 375 with
   the old &nbsp; separators, two wrapped rows sat 19px apart and their boxes
   overlapped by 29px: axe target-size fails an overlapping PAIR even when both
   targets are individually large enough, and it reported the failure against
   the Privacy link, which had not changed. Anything under about 29px here
   reintroduces it. */
.foot-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: var(--s-3);
  row-gap: var(--s-4);
}

/* padding-inline, NOT padding. This button also carries .tap, which buys the
   44px hit area with `padding-block: 14px; margin-block: -14px`. A blanket
   `padding: 0` here is later in the cascade and silently cancels it: the
   rendered target measured 129 by 20 and scripts/verify.mjs failed it on two
   pages. Zero the inline padding only, and .tap keeps working. */
.foot-btn {
  border: 0;
  background: none;
  padding-inline: 0;
  font: inherit;
  color: var(--orange-ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  cursor: pointer;
}
.foot-btn:hover {
  text-decoration-thickness: 2px;
  text-decoration-color: var(--werk-orange);
}

/* Under 750px the two buttons go full width on their own row. Side by side at
   375 they would be about 140px each, which clears the 44px tap floor on
   height but leaves two small targets adjacent to each other, and adjacency is
   what produces mis-taps. Stacked, each is the full measure. */
@media (max-width: 749px) {
  .consent-inner {
    flex-direction: column;
    gap: var(--s-2);
    padding-block: var(--s-2);
  }
  .consent-acts {
    width: 100%;
  }
  .consent-btn {
    flex: 1;
  }
}
