/* ==========================================================================
   G R SITES — components.css
   Implements every block in docs/design-system.md §5, including the six
   used only by advertise.html: config panel, cart drawer, price table,
   stepper, chip selector, callout.

   Flat BEM: .block, .block__element, .block--modifier.
   Classes own ALL spacing on this site (see base.css §DISCIPLINE).
   No raw hex, no raw px in this file.

   CONTENTS
     1  Grounds — .band
     2  Layout — .manifest
     3  Shell — .masthead, .nav, .colophon
     4  Typographic — .pagehead, .eyebrow, .caps, .lede, .prose, .marginal
     5  Rules — .rule--hair / --section / --sig / --channel
     6  Motif — .motif, .channels
     7  Registers — .ledger and its four modifiers, .entry, .link--out, .tag
     8  Blocks — .contents, .tally, .disclosure-note, .callout, .stepline,
                 .faq, .clause
     9  Controls — .btn, .form, .control, .checkbox, .filterbar
    10  advertise.html only — .cart-tab, .cart, .config, .stepper, .chipset,
                 .pack, .surcharge, .orderref, .orderdump, .badge--sponsored
    11  State utilities
    12  Motion — the shared entrance layer and the reduced-motion contract.
        The micro-interactions themselves are NOT here: each one lives in
        the component that already owns the property it animates (.nav__link
        in §3, .ledger__row in §7, .btn in §9), because a second block
        setting `transition` on a selector cancels the first. §12 owns the
        contract, the reveal engine, and the two signature draw-ins.
   ========================================================================== */


/* ==========================================================================
   1. GROUNDS
   A band reassigns the ground system, so no component below ever needs to
   know what it has landed on. Four grounds, three of them declared here.
   ========================================================================== */

.band {
  background-color: var(--ground);
  color: var(--fg);
  padding-block: var(--sp-3);
}

@media (min-width: 48rem) {
  .band {
    padding-block: var(--sp-4);
  }
}

.band--white {
  --ground: var(--white);
}

.band--surface {
  --ground: var(--surface);
}

/* The record ground. Bands are always taller than light sections so the band
   reads as an event rather than as another stripe. */
.band--ink {
  --ground: var(--ink);
  --fg: var(--on-ink);
  --fg-mute: var(--on-ink);      /* NO muted tone on ink — by design */
  --fg-accent: var(--cyan);
  --link: var(--on-ink);
  --rule: var(--rule-on-ink);
  --rule-strong: var(--rule-on-ink-strong);
  --focus-c: var(--cyan);
  --motif-stroke-c: var(--rule-on-ink-strong);
  --motif-chip-c: var(--cyan);   /* the only place cyan enters a band */
  padding-block: var(--sp-4);
}

@media (min-width: 64rem) {
  .band--ink {
    padding-block: var(--sp-6);
  }
}

/* The band above renders as one block with the band below it. The rule
   directly beneath already stops adjacent same-ground bands doubling their
   padding; this is for the stronger case, where the two bands are not two
   sections at all but one continuous unit that happens to need two elements.
   FIRST USE, titles.html (2026-08-10): the pagehead band closes on the
   signature rule and the domain-history disclosure opens the very next band,
   and the disclosure is written to be read as part of the head, not as a
   section after it. The rule's own --sp-2 bottom margin is the whole
   separator; --sp-4 of band padding underneath it put 198px between a 6px
   rule and the note it introduces. */
.band--flush {
  padding-block-end: 0;
}

/* A band that is a RAIL, not a section: no heading of its own, one line of
   content, read as evidence attached to the band above rather than as the
   next thing on the page. FIRST USE, advertise.html (2026-08-10): the
   at-a-glance tally strip is four figures with an aria-label and no <h2>,
   and a full section's --sp-4 on both sides wrapped 211px of padding around
   56px of content — the worst ratio on that page. --sp-2 is the same
   separator .rule--sig already uses to close a block, so the strip sits
   under the hero as its footing instead of floating between two sections. */
.band--rail {
  padding-block: var(--sp-2);
}

/* Adjacent bands on the SAME ground must not double their padding. The band
   system assumes grounds alternate, but a prose page legitimately runs
   several sections on white in a row — and two --sp-4 paddings meeting
   produced ~211px of dead space with no change of ground to justify it.
   One band's bottom padding is the whole separator.

   `:not(.no-js-only)` is the display:none correction, added 2026-08-10 with
   advertise.html — the one page that toggles whole bands by rendering path.
   A `+` combinator counts DOM siblings, not painted ones, so a hidden band
   made this rule fire across a band it was never true of: with JS on, the
   price-list band is display:none and the section after it was having its
   opening padding zeroed against the SURFACE band two siblings up. That is
   a change of ground, which is precisely the case this rule exempts. With
   the :not() the band simply keeps .band's own padding — no value is
   restated here, so it stays correct at both breakpoints. */
.band--white:not(.no-js-only) + .band--white,
.band--surface + .band--surface,
.band--ink + .band--ink {
  padding-block-start: 0;
}

/* The same defeat, in the other direction and on the other rendering path.
   With JS off every .js-only band between the price list and the plain-words
   section is display:none, so two white bands meet visually while being six
   siblings apart in the DOM, and the rule above cannot see them: 211px of
   same-ground dead space, the exact failure it exists to kill. `~` rather
   than `+` is what reaches across the hidden run, and it is safe to let it
   reach the whole tail — under .no-js every white band after the price list
   is either hidden or already zeroed by the rule above. */
.no-js .band--white.no-js-only ~ .band--white {
  padding-block-start: 0;
}

.band--ink a:hover {
  text-decoration-thickness: var(--rule-section);
}


/* ==========================================================================
   2. LAYOUT — the manifest grid
   Not a 12-column grid. A catalogue grid: accession gutter, content measure,
   marginal column.
   ========================================================================== */

.manifest {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 var(--sp-1);
  max-inline-size: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--sp-1);
}

.band__inner {
  max-inline-size: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--sp-1);
}

/* Vertical flow inside a band. base.css zeroes every element margin, so a
   block sitting directly in a band has no rhythm of its own — the band
   supplies it. Without this, anything not wrapped in .prose stacks flush.
   Margins collapse against .eyebrow's own bottom margin rather than adding
   to it, which is why the eyebrow needs no special case. */
.band__inner > * + * {
  margin-block-start: var(--sp-1);
}

.band__inner > :is(h1, h2) + * {
  margin-block-start: var(--sp-1h);
}

.band__inner > .lede {
  margin-block-start: var(--sp-1h);
}

.band__inner > .channels-line {
  margin-block-start: var(--sp-2);
}

/* The same rhythm for the catalogue grid, supplied as a row gap so it does
   not fight the accession column's alignment. */
.manifest {
  row-gap: var(--sp-1);
}

@media (min-width: 48rem) {
  .manifest,
  .band__inner {
    padding-inline: var(--sp-2);
  }
}

@media (min-width: 64rem) {
  .manifest {
    grid-template-columns: var(--gutter) minmax(0, var(--measure)) var(--gutter-note);
  }

  .manifest > * {
    grid-column: 2;
  }

  .manifest > .accession {
    grid-column: 1;
    text-align: end;
  }

  .manifest > .marginal {
    grid-column: 3;
  }

  .manifest > .full {
    grid-column: 1 / -1;
  }
}

/* Below 64rem the accession collapses inline above its heading and
   marginalia become inset notes. */
.accession {
  display: block;
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
  color: var(--fg-mute);
  font-variant-numeric: tabular-nums;
}

.marginal {
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
  border-inline-start: var(--rule-section) solid var(--rule-strong);
  padding-inline-start: var(--sp-h);
}

@media (max-width: 63.999rem) {
  .marginal {
    margin-block-start: var(--sp-1);
  }
}


/* ==========================================================================
   3. SHELL
   ========================================================================== */

/* --- 3.1 masthead — fixed height, white ground, on all seven pages ------- */

.masthead {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-masthead);
  background-color: var(--white);
  color: var(--ink);
  border-block-end: var(--rule-section) solid var(--teal-deep);
}

.masthead__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-1);
  block-size: var(--masthead-h);
  max-inline-size: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--sp-1);
}

@media (min-width: 48rem) {
  .masthead__inner {
    padding-inline: var(--sp-2);
  }
}

.masthead__lockup {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-h);
  /* The mark is 39.6px tall and the link was wrapping it exactly, so the
     home link on all seven pages was a 40px target against a 44px floor.
     The masthead is --sp-3 (79.2px) tall, so this costs no layout. */
  min-block-size: var(--tap);
  color: var(--ink);
  text-decoration: none;
  flex: 0 0 auto;
}

/* assets/logos/svg/mark-tight-pine.svg — 51.9 x 39.6px.
   The tight cut (viewBox 8 18.8 108 82.4) fills the box; the padded
   mark-pine.svg would render the glyph at ~30px in the same height and read
   undersized beside the tracked caps. Flat pine: no ramp and no cyan chip on
   a white masthead, so the gradient budget and the cyan budget are both
   untouched by the header. See docs/logo-usage-plan.md §1.1. */
.masthead__mark {
  display: block;
  block-size: var(--sp-1h);
  inline-size: auto;
  flex: 0 0 auto;
}

.masthead__word {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-fine);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
}

/* --- 3.2 nav ------------------------------------------------------------ */

.nav__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-h);
  list-style: none;
}

/* Sentence case, not tracked caps: at 0.14em the labels push past a
   comfortable 44px target grid on mobile. */
.nav__link {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--tap);
  padding-inline: var(--sp-h);
  font-size: var(--fs-fine);
  color: var(--ink);
  text-decoration: none;
  /* MICRO-INTERACTION (motion layer, §12 states the contract).
     Hover is a channel stroke growing under the label — one solid
     linear-gradient() layer, the same technique .motif paints with, so the
     affordance costs no pseudo-element, no DOM and no layout. It replaces
     the hover underline rather than joining it: two rules under one label
     is a bug, and this one lands on the same bottom edge, in the same
     weight and the same teal, as the [aria-current] rule below — so the
     hover state is legibly a preview of the current-page state.
     Under `reduce` the transition below simply does not exist and the rule
     appears at full width on hover: the affordance is untouched. */
  background-image: linear-gradient(var(--teal) 0 0);
  background-repeat: no-repeat;
  background-position: left bottom;
  background-size: 0 var(--rule-section);
}

.nav__link:hover {
  background-size: 100% var(--rule-section);
}

@media (prefers-reduced-motion: no-preference) {
  .nav__link {
    transition: background-size var(--dur) var(--ease);
  }
}

/* Forced colours discards background images, which would leave the nav with
   no hover affordance at all. The underline comes back there, and only
   there — it is the fallback, not the design. */
@media (forced-colors: active) {
  .nav__link:hover {
    text-decoration-line: underline;
    text-decoration-thickness: var(--rule-section);
    text-underline-offset: 0.25em;
  }
}

.nav__link[aria-current="page"] {
  font-weight: var(--weight-bold);
  box-shadow: inset 0 calc(var(--rule-section) * -1) 0 0 var(--teal);
}

.nav__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--tap);
  min-block-size: var(--tap);
}

/* 64rem, not 48rem. Six full nav labels need ~536px and there is ~497px at
   768 — measured, and it wrapped inside the fixed-height masthead. Rather
   than truncate "Editorial standards" (the label an ad-network reviewer is
   looking for), the shell stays in its compact presentation until 64rem.
   This is the SAME breakpoint the register uses, so the whole shell changes
   mode once rather than twice. */
@media (min-width: 64rem) {
  .nav__toggle {
    display: none;
  }
}

/* With JS off the list is simply visible and the toggle is not rendered at
   all — it governs nothing, so it must not be offered. */
@media (max-width: 63.999rem) {
  html.no-js .nav__toggle {
    display: none;
  }

  html:not(.no-js) .nav__list {
    display: none;
  }

  html:not(.no-js) .nav.is-open .nav__list {
    display: flex;
    position: absolute;
    inset-inline: 0;
    inset-block-start: var(--masthead-h);
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--sp-h) var(--sp-1) var(--sp-1);
    background-color: var(--white);
    border-block-end: var(--rule-section) solid var(--teal-deep);
  }
}

/* --- 3.3 colophon — --ink on every page, and always one of the bands ----- */

.colophon {
  --ground: var(--ink);
  --fg: var(--on-ink);
  --fg-mute: var(--on-ink);
  --fg-accent: var(--cyan);
  --link: var(--on-ink);
  --rule: var(--rule-on-ink);
  --rule-strong: var(--rule-on-ink-strong);
  --focus-c: var(--cyan);
  --motif-stroke-c: var(--rule-on-ink-strong);
  --motif-chip-c: var(--cyan);
  background-color: var(--ground);
  color: var(--fg);
  padding-block: var(--sp-3);
}

.colophon__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-2) var(--sp-1);
  max-inline-size: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--sp-1);
}

@media (min-width: 48rem) {
  .colophon__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    padding-inline: var(--sp-2);
  }
}

@media (min-width: 64rem) {
  .colophon__grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* assets/logos/svg/mark-frost.svg — 79.2px box, on the colophon's --ink
   ground at 10.81:1. It stands in place of a .colophon__head in the imprint
   column: the printer's device at the head of the imprint, which is what a
   colophon mark is for. Flat frost — no ramp, no cyan — so the colophon's one
   cyan mark stays the channel-motif chip in .colophon__channels.
   The box is --sp-3 rather than --sp-2 because the kit has no tight frost
   cut; the padded viewBox renders the glyph at ~60 x 46px inside it.
   See docs/logo-usage-plan.md §1.2 and Open Question 4. */
.colophon__mark {
  display: block;
  block-size: var(--sp-3);
  inline-size: auto;

  /* mark-frost.svg is the PADDED cut (viewBox -9 -11 142 142). Measured, its
     ink occupies only the middle 108 x 82.4 — 12.01% dead space on each side
     and 20.99% top and bottom. Left untrimmed the glyph sits ~9.5px inboard
     of the column edge while every other colophon head starts flush, which
     is plainly visible against a four-column grid.
     These are ratios of the box read straight off the viewBox, not magic
     pixels. Delete all three the day a tight frost cut lands — see
     docs/logo-usage-plan.md Open Question 4. */
  --mark-pad-x: 0.1201;
  --mark-pad-y: 0.2099;
  margin-inline-start: calc(var(--sp-3) * var(--mark-pad-x) * -1);
  margin-block-start: calc(var(--sp-3) * var(--mark-pad-y) * -1);
  margin-block-end: calc(var(--sp-1) - var(--sp-3) * var(--mark-pad-y));
}

.colophon__head {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
  padding-block-end: var(--sp-h);
  margin-block-end: var(--sp-h);
  border-block-end: var(--hair) solid var(--rule);
}

.colophon__list,
.colophon__routes,
.colophon__index {
  list-style: none;
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
}

/* Every colophon link is a 44px target, and that height IS the spacing —
   an extra li margin on top of it made the Routes column read as twice as
   loose as the Contents column beside it. Addresses and page names must
   also wrap rather than force the column wide on a 320px screen. */
.colophon__list a,
.colophon__routes a,
.colophon__index a {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--tap);
  word-break: break-word;
}

.colophon__index {
  counter-reset: colophon-index;
}

.colophon__index > li {
  counter-increment: colophon-index;
  display: flex;
  align-items: center;
  gap: var(--sp-h);
}

.colophon__index > li::before {
  content: counter(colophon-index, decimal-leading-zero);
  color: var(--fg-mute);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}

.colophon__imprint {
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
}

.colophon__channels {
  padding-block-end: var(--sp-2);
}

.colophon__legal {
  max-inline-size: var(--wrap);
  margin-inline: auto;
  margin-block-start: var(--sp-2);
  padding-inline: var(--sp-1);
  padding-block-start: var(--sp-1);
  border-block-start: var(--hair) solid var(--rule);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
}

@media (min-width: 48rem) {
  .colophon__legal {
    padding-inline: var(--sp-2);
  }
}


/* ==========================================================================
   4. TYPOGRAPHIC
   ========================================================================== */

.pagehead {
  padding-block: var(--sp-3) var(--sp-2);
}

/* A .pagehead is always the first block inside the first .band of its page,
   between the band's own --sp-4 opening padding and whatever rule closes the
   head — and it adds --sp-3 / --sp-2 of its own on top of both. Measured on
   titles.html at 1440 that put 184.8px between the masthead rule and the
   accession line, a fifth of the fold spent before the first word on a page
   whose entire job is a fifteen-row lookup, and left the signature rule with
   105.6px above it against 52.8px below — a divider sitting closer to the
   section it introduces than the one it closes reads as misaligned even when
   the binding is right.

   This drops BOTH helpings and lets the neighbours' own spacing govern: the
   band's padding is the whole opening, and the rule's native --sp-2 margin
   is the whole gap on each side of it, which is the symmetry §5 and §6.2
   already declare. Opt-in per page, because the other four pages carrying
   .pagehead have not been through this pass yet. */
.pagehead--flush {
  padding-block: 0;
}

.pagehead--index {
  --h1-size: var(--fs-masthead);
  --h1-leading: var(--leading-masthead);
}

@media (max-width: 39.999rem) {
  .pagehead--index {
    --h1-size: var(--fs-title);
    --h1-leading: var(--leading-title);
  }
}

.pagehead__accession {
  display: block;
  margin-block-end: var(--sp-1);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
  color: var(--fg-mute);
  font-variant-numeric: tabular-nums;
}

.pagehead__standfirst {
  max-inline-size: var(--measure);
  margin-block-start: var(--sp-1);
  font-size: var(--fs-lede);
  line-height: var(--leading-lede);
}

.pagehead__meta {
  /* The standfirst above it is measure-constrained and this was not, so a
     two-line note set as one 1180px line directly under a 66ch paragraph —
     the wider line being the SMALLER type, which reads as a mistake because
     it is one. Every text block on this site holds the measure. */
  max-inline-size: var(--measure);
  margin-block-start: var(--sp-1);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
}

/* --- the tracked-caps treatment ----------------------------------------- */
/* Permitted: .eyebrow, .btn labels, .ledger__head, .pagehead__accession,
   .tag, .badge--sponsored. Never body copy, never over 28 characters,
   never wrapping to three lines, never headings, never prose links. */

.caps,
.eyebrow {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  /* At 0.14em every tracked string carries a phantom space after its last
     letter. Without this trim, a ledger-aligned caps label sits visibly off
     its column. This is the whole difference between a tracked-caps
     treatment and tracked-caps applied. */
  margin-inline-end: calc(var(--track-caps) * -1);
  font-feature-settings: "case" 1;
}

.caps--micro {
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
}

.eyebrow {
  display: block;
  margin-block-end: var(--sp-h);
  color: var(--fg-mute);
}

.lede {
  max-inline-size: var(--measure);
  font-size: var(--fs-lede);
  line-height: var(--leading-lede);
}

.prose {
  max-inline-size: var(--measure);
}

.prose > * + * {
  margin-block-start: var(--sp-1);
}

.prose > :is(h2, h3) {
  margin-block-start: var(--sp-2);
}

.prose > :is(h2, h3) + * {
  margin-block-start: var(--sp-1h);
}

.prose :is(ul, ol) {
  padding-inline-start: var(--sp-1h);
}

.prose li + li {
  margin-block-start: var(--sp-h);
}


/* ==========================================================================
   5. RULES — three weights, each encoding one thing.
   A rule that separates nothing is forbidden.
   ========================================================================== */

.rule--hair {
  block-size: var(--hair);
  background-color: var(--rule);
}

.rule--section {
  block-size: var(--rule-section);
  background-color: var(--rule-strong);
  margin-block: var(--sp-1);
}

/* The page signature. ONCE per page, at the page's structural pivot.
   One of the two permitted gradient uses. */
.rule--sig {
  block-size: var(--rule-sig);
  background-image: var(--gradient);
  margin-block: var(--sp-1) var(--sp-2);
}


/* ==========================================================================
   6. THE CHANNEL MOTIF
   One grammar — n equal strokes on a fixed pitch, one chip offset on the
   last. Three implementations. EXACTLY THREE INSTANCES PER PAGE, counting
   all three implementations together.

   COMPOSITION RULE, verified by render: every motif instance that lands on
   an ink ground paints one cyan chip. All three on dark = 3 cyan marks,
   which is inside the max-4 ceiling but well past the intended budget of 1.
   So: AT MOST TWO motif instances per page may sit on an ink ground. On a
   light ground the chip is --teal-deep and costs nothing against the cyan
   budget. Check this when composing each page, not after.
   ========================================================================== */

/* --- 6.1 Implementation A — .motif (marker, zero extra DOM) -------------- */
/* Four solid linear-gradient() layers on one ::before. No image request,
   no pseudo-element stacking. Section-head marker, and the cart empty state. */

.motif::before {
  content: "";
  display: block;
  /* width accounts for the chip overhanging the third stroke */
  inline-size: var(--motif-w);
  block-size: var(--motif-h);
  margin-block-end: var(--sp-h);
  background-image:
    linear-gradient(var(--motif-stroke-c) 0 0),
    linear-gradient(var(--motif-stroke-c) 0 0),
    linear-gradient(var(--motif-stroke-c) 0 0),
    linear-gradient(var(--motif-chip-c) 0 0);
  background-repeat: no-repeat;
  background-size:
    var(--motif-hair) 100%,
    var(--motif-hair) 100%,
    var(--motif-hair) 100%,
    var(--motif-chip) var(--motif-chip);
  /* The three stroke layers are anchored to the BOTTOM edge rather than the
     top. At full height the two are identical renders — this is the static
     form, unchanged. It matters only to §12, where the strokes draw from
     zero: bottom-anchored, they grow upward out of the baseline, which is
     the same direction the hero mark rises and the same direction a band
     arrives. Top-anchored they would drip downward, which is the one
     gesture on this site that would have to mean something and doesn't. */
  background-position:
    left 0 bottom 0,
    left var(--motif-gap) bottom 0,
    left calc(var(--motif-gap) * 2) bottom 0,
    calc(var(--motif-gap) * 2 + var(--motif-chip-nudge)) 62%;
}

/* --- 6.2 Implementation B — .rule--channel (section break, parameterised) */
/* repeating-linear-gradient generates the strokes, so the count is a single
   multiplier: set --motif-n: 15 and the same rule renders the full comb. */

.rule--channel {
  block-size: var(--motif-h);
  margin-block: var(--sp-2);
  background-image:
    linear-gradient(var(--rule) 0 0),
    repeating-linear-gradient(
      90deg,
      var(--motif-stroke-c) 0 var(--motif-hair),
      transparent var(--motif-hair) var(--motif-gap)
    ),
    linear-gradient(var(--motif-chip-c) 0 0);
  background-repeat: no-repeat;
  background-size:
    100% var(--hair),
    calc(var(--motif-gap) * var(--motif-n)) 100%,
    var(--motif-chip) var(--motif-chip);
  /* The comb is anchored to the BOTTOM edge, for the same reason .motif's
     three strokes are (§6.1): the layer is 100% of a --motif-h box, so at
     full height `top 0` and `bottom 0` are the identical render and this
     changes nothing statically. It matters only to §12, where the comb draws
     from zero height — bottom-anchored it grows upward out of the baseline,
     which is the direction a band arrives; top-anchored it would drip down. */
  background-position:
    left 0 top 50%,
    right 0 bottom 0,
    right var(--motif-chip-inset) top 62%;
}

.rule--channel--wide {
  --motif-n: 15;
}

/* The signature rule carrying the motif terminal. Counts as BOTH the page's
   one .rule--sig and one of its three motif instances — use one or the
   other, never both on the same page. */
.rule--channel--sig {
  background-image:
    var(--gradient),
    repeating-linear-gradient(
      90deg,
      var(--motif-stroke-c) 0 var(--motif-hair),
      transparent var(--motif-hair) var(--motif-gap)
    ),
    linear-gradient(var(--motif-chip-c) 0 0);
  background-size:
    100% var(--rule-sig),
    calc(var(--motif-gap) * var(--motif-n)) 100%,
    var(--motif-chip) var(--motif-chip);
}

/* --- 6.2b .hero-mark — the homepage motion moment (index hero only) -----
   OWNER DECISION 2026-08-09: use an animated logo on the homepage.
   DELIVERED, but not with the kit's animated SVG. Reason, measured:

   Every shape in `animated/mark-spine-rise-reversed.svg` starts hidden —
   clip rects at width 0, the bowl mask at stroke-dashoffset 160 — and is
   revealed BY the SMIL animation. Tested in this engine, inline and with
   the SVG timeline confirmed running at 3.78s, the clip rects were STILL 0.
   SMIL never executed. So the file's failure mode is not "no animation";
   it is "no logo at all" — a 158px hole in the hero.

   That is unshippable regardless of preference, so the motion is delivered
   the way everything else on this site is: the artefact is correct first,
   and the animation is pure enhancement on top.

   · src is the STATIC mark-frost.svg — the same cut the colophon uses, so
     "mark on an ink ground is frost" is now one consistent site rule.
     mark-colour-on-pine.svg was tried first and rejected on sight: its ramp
     STARTS at #123F3F, which is the band colour, so the left of the G
     dissolved into the ground. The animated cut avoided that with a
     different gradient (#2C7375 to #62B4B9); no static file reproduces it.
     Flat frost is 10.81:1 on ink and fully legible. Always visible.
   · The draw-on is a CSS one-shot: a wipe from the baseline plus a small
     rise, ~2.4s, matching the kit's intended timing. `animation-fill-mode:
     both` guarantees it ends visible.
   · Wrapped in prefers-reduced-motion: no-preference. Honoured natively —
     no <picture> source-swap needed, because the static file IS the src.
   · If CSS animation is unavailable or blocked, the mark simply shows.

   BONUS from the frost cut: it carries no cyan chip, so the hero viewport
   is back to exactly ONE cyan mark — the arrival register — which is the
   designed budget. The animated route would have cost two. */
.hero-mark {
  display: block;
  block-size: var(--sp-4);          /* 105.6px — clears the kit's 96px floor */
  inline-size: auto;
  margin-block-end: var(--sp-1h);
}

@media (min-width: 64rem) {
  .hero-mark {
    block-size: var(--sp-6);        /* 158.4px */
  }
}

@media (prefers-reduced-motion: no-preference) {
  .hero-mark {
    animation: hero-mark-rise 2.4s var(--ease) both;
  }

  @keyframes hero-mark-rise {
    from {
      clip-path: inset(100% 0 0 0);
      transform: translateY(var(--sp-h));
      opacity: 0;
    }
    60% {
      opacity: 1;
    }
    to {
      clip-path: inset(0 0 0 0);
      transform: translateY(0);
      opacity: 1;
    }
  }
}

/* --- 6.3 Implementation C — .channels (the register — motif as data) ----- */
/* Fifteen strokes, one per property, in accession order. When a reader
   arrives from one of the fifteen, that stroke brightens and takes the chip.
   DOM-backed because the strokes are addressable data, not decoration.
   Progressive enhancement: with JS off, fifteen even strokes and a true
   static caption. The register stays aria-hidden — the meaning is in the
   sentence, the picture is the picture. */

.channels-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-h) var(--sp-1);
}

.channels {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: var(--motif-hair);
  gap: calc(var(--motif-gap) - var(--motif-hair));
  block-size: var(--motif-h);
  inline-size: max-content;
  flex: 0 0 auto;
}

.channels > i {
  background-color: var(--motif-stroke-c);
  position: relative;
}

.channels > i[data-here] {
  background-color: var(--fg);   /* 10.85:1 on ink — the bright stroke */
}

.channels > i[data-here]::after {
  content: "";
  position: absolute;
  inline-size: var(--motif-chip);
  block-size: var(--motif-chip);
  inset-block-start: 62%;
  inset-inline-start: var(--motif-chip-nudge);
  background-color: var(--motif-chip-c);
}

@media (prefers-reduced-motion: no-preference) {
  .channels > i {
    transition: background-color var(--dur-fast) linear;
  }
}

.channels-caption {
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
}


/* ==========================================================================
   7. REGISTERS — the card replacement
   There is not one drop-shadowed rounded rectangle floating on a ground
   anywhere in this system.
   ========================================================================== */

.ledger {
  inline-size: 100%;
  list-style: none;
}

.ledger__head .ledger__cell,
.ledger__head th {
  padding-block: var(--sp-h);
  padding-inline-end: var(--sp-1);
  border-block-end: var(--rule-section) solid var(--rule-strong);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--fg-mute);
  white-space: nowrap;
}

.ledger__cell {
  padding-block: var(--sp-1);
  padding-inline-end: var(--sp-1);
  border-block-end: var(--hair) solid var(--rule);
  vertical-align: baseline;
}

.ledger__cell:last-child {
  padding-inline-end: 0;
}

/* Group heads inside a register — subject areas on titles.html. */
.ledger__group .ledger__cell {
  padding-block: var(--sp-1h) var(--sp-h);
  border-block-end: var(--rule-section) solid var(--rule-strong);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--fg-mute);
}

/* Row hover: surface fill + a 2px --teal inline-start rule. Not a card. */

/* MICRO-INTERACTION (motion layer, §12 states the contract). The fill and
   the rule fade in together rather than snapping, which is what stops a
   fifteen-row register from flickering as a pointer crosses it. The rule is
   deliberately the FASTER of the two: the marker that says "this row" wants
   to be already there when the eye arrives, while the ground it sits on can
   take its time. Transitioned on the cell because that is where the rule is
   painted — a <tr> box-shadow does not render in every engine.
   Both properties are hover-only paint: under `reduce` the state change is
   instant and identical in every other respect. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .ledger__body .ledger__row {
    transition: background-color var(--dur) var(--ease);
  }

  .ledger__body .ledger__row .ledger__cell:first-child {
    transition: box-shadow var(--dur-fast) var(--ease);
  }
}

@media (hover: hover) {
  .ledger__body .ledger__row:hover {
    background-color: var(--surface);
  }

  .ledger__body .ledger__row:hover .ledger__cell:first-child {
    box-shadow: inset var(--rule-section) 0 0 0 var(--teal);
  }

  .band--ink .ledger__body .ledger__row:hover {
    background-color: transparent;
  }

  .band--ink .ledger__body .ledger__row:hover .ledger__cell:first-child {
    box-shadow: inset var(--rule-section) 0 0 0 var(--cyan);
  }
}

/* --- 7.1 .ledger--titles — index + titles -------------------------------- */
/* ACCESSION · TITLE · DOMAIN · SUBJECT · IN NETWORK SINCE */

.ledger--titles .ledger__head {
  position: sticky;
  inset-block-start: var(--masthead-h);
  z-index: 1;
}

/* The ground must be painted on the CELLS. With border-collapse: separate a
   background on <thead> does not paint in every engine, and rows scrolling
   underneath a sticky head then show through it. */
.ledger--titles .ledger__head .ledger__cell {
  background-color: var(--ground);
}

/* This page is read one row at a time. */
.ledger--titles .ledger__cell {
  padding-block: var(--sp-1h);
}

/* In table mode there is room to keep a domain intact; breaking one across
   two lines makes it unreadable as an address, which is the one thing this
   column exists to be. Below 48rem the row is stacked and wraps normally. */
@media (min-width: 64rem) {
  .ledger--titles .entry__domain {
    white-space: nowrap;
  }

  /* .ledger--expanded is titles.html's form of the register: the same five
     columns, but the TITLE cell also carries the beat and a one-sentence
     description. Auto table layout divides width by content, which starved
     that column — the sentence set at ~230px, four words to a line, while
     three short columns held width they had no use for. Half the table is
     the whole difference between a catalogue entry and a squeezed cell.
     index.html's register keeps the default: one string per cell, nothing
     to starve.

     Two columns are claimed, not one. Giving TITLE half and letting the
     rest fall out of auto layout starved BEAT instead — a 57-character
     phrase in a sixty-pixel column, four lines of two words. Both columns
     that hold a phrase get a share; the three that hold a number, an
     address and a chip take what is left, which is all they need. */
  .ledger--titles.ledger--expanded .entry__title,
  .ledger--titles.ledger--expanded .ledger__head .ledger__cell:nth-child(2) {
    inline-size: 40%;
  }

  .ledger--titles.ledger--expanded .entry__beat,
  .ledger--titles.ledger--expanded .ledger__head .ledger__cell:nth-child(4) {
    inline-size: 22%;
  }
}

.entry__acc {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-caps);
  color: var(--fg-mute);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.entry__title {
  font-family: var(--font-display);
  font-size: var(--fs-sub);
  line-height: var(--leading-sub);
}

/* index.html puts one string in this cell. titles.html puts two — the name
   and a one-sentence description — because the register IS that page rather
   than a summary on it.

   Three things have to be undone here, all inherited rather than asked for.
   base.css zeroes every element margin, so the stack has no rhythm of its
   own. The cell is a <th>, so base.css's `th { font-weight: bold }` reached
   the description and set the sentence in semibold. And .entry__title sets
   the DISPLAY face, which the description inherited — so a one-sentence
   note rendered as a second heading. The name is the heading; the line
   under it is text, and has to be told so three times. */
.entry__title > * {
  display: block;
  margin-block-start: var(--sp-q);
  font-family: var(--font-body);
  font-weight: var(--weight-body);
}

.entry__domain {
  font-size: var(--fs-fine);
  word-break: break-word;
}

/* Always a block. It carries max-inline-size, which an inline box ignores
   entirely — so a .entry__desc set on a <span> silently ran the full width
   of its cell on contact.html while looking correct in review. */
.entry__desc {
  display: block;
  max-inline-size: var(--measure);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
}

/* The beat, from docs/portfolio.json. Deliberately NOT a .tag: the evidenced
   beats run to 57 characters ("Online Marketplaces, Scams & Consumer
   Protection (India)"), and the tracked-caps rule is short strings only —
   never over ~28 characters, never wrapping to three lines. Set as plain
   sentence-case text instead, which is what a phrase wants. */
.entry__beat {
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
  text-wrap: pretty;
}

.entry__meta {
  font-size: var(--fs-fine);
  color: var(--fg-mute);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Reflow, not a second DOM. Below 48rem each row becomes a stacked .entry
   with its column head restored from data-head. The accession stays visible
   in every collapsed entry — the number is what says RECORD rather than
   CARD, and it is the whole mitigation for the risk logged in §7 of
   docs/design-system.md. If /shoot at 375px says otherwise, redesign this
   block, not the desktop register. */
/* 64rem, not 48rem. Measured at exactly 768: the five-column register
   renders 930px wide and overflows the viewport. A five-column manifest
   needs desktop width; below it, the stacked entry IS the register. */
@media (max-width: 63.999rem) {
  .ledger--titles,
  .ledger--data,
  .ledger--routes {
    display: block;
  }

  /* Column heads move into each cell via data-head; the thead itself is
     hidden from sight but kept in the accessibility tree. */
  .ledger--titles .ledger__head,
  .ledger--data .ledger__head,
  .ledger--routes .ledger__head {
    position: absolute;
    inline-size: var(--hair);
    block-size: var(--hair);
    overflow: hidden;
    clip-path: inset(50%);
  }

  .ledger--titles .ledger__body,
  .ledger--data .ledger__body,
  .ledger--routes .ledger__body {
    display: block;
  }

  .ledger--titles .ledger__row,
  .ledger--data .ledger__row,
  .ledger--routes .ledger__row {
    display: block;
    padding-block: var(--sp-1h);
    border-block-end: var(--hair) solid var(--rule);
  }

  .ledger--titles .ledger__cell,
  .ledger--data .ledger__cell,
  .ledger--routes .ledger__cell {
    display: block;
    padding-block: 0;
    padding-inline-end: 0;
    border-block-end: 0;
  }

  .ledger--titles .ledger__cell + .ledger__cell,
  .ledger--data .ledger__cell + .ledger__cell,
  .ledger--routes .ledger__cell + .ledger__cell {
    margin-block-start: var(--sp-h);
  }

  .ledger__cell[data-head]::before {
    content: attr(data-head);
    display: block;
    font-family: var(--font-display);
    font-weight: var(--weight-caps);
    font-size: var(--fs-micro);
    line-height: var(--leading-micro);
    text-transform: uppercase;
    letter-spacing: var(--track-caps);
    color: var(--fg-mute);
  }

  /* Four labels are self-evident from their own content and would cost a
     line each across fifteen stacked entries — a domain reads as a domain
     and a tag reads as a subject without being announced. "In network
     since" stays, because a bare year does not explain itself. Keep
     data-head on every cell for the accessibility tree; just don't paint
     the redundant ones. */
  .entry__acc[data-head]::before,
  .entry__title[data-head]::before,
  .entry__domain[data-head]::before,
  .entry__beat[data-head]::before,
  .ledger__cell[data-head]:has(.tag)::before {
    content: none;
  }

  /* Domain takes its own line; the beat is a full phrase and takes another.
     The year keeps its label because a bare "2009" does not explain itself,
     set inline so label and value read as one item rather than two lines. */
  .ledger--titles .entry__domain,
  .ledger--titles .ledger__cell:has(.tag),
  .ledger--titles .entry__meta {
    display: inline-block;
    margin-inline-end: var(--sp-1);
  }

  .ledger--titles .entry__meta[data-head]::before {
    display: inline;
    margin-inline-end: var(--sp-q);
  }

  .ledger__group .ledger__cell {
    padding-block: var(--sp-1h) var(--sp-h);
  }
}

/* --- 7.2 .ledger--rate — the price table (advertise.html) ---------------- */
/* A list, not a table: name, dotted leader, price. The contents-page device.
   NOT three tier cards. NOT a "Most Popular" ribbon. */

.ledger--rate .ledger__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-h);
  padding-block: var(--sp-1);
  border-block-end: var(--hair) solid var(--rule);
}

.rate__name {
  font-family: var(--font-display);
  font-size: var(--fs-lede);
  line-height: var(--leading-lede);
  flex: 0 1 auto;
}

.rate__leader {
  flex: 1 1 var(--sp-2);
  min-inline-size: var(--sp-2);
  border-block-end: var(--hair) dotted var(--rule-strong);
  translate: 0 -0.3em;
}

.rate__price {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-sub);
  line-height: var(--leading-sub);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}

.rate__unit {
  font-size: var(--fs-fine);
  color: var(--fg-mute);
  flex: 0 0 auto;
}

.rate__includes {
  flex: 1 1 100%;
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
}

.rate__action {
  flex: 0 0 auto;
  margin-inline-start: auto;
}

/* --- 7.3 .ledger--data — privacy.html's manifest ------------------------- */
/* WHAT · WHY · WHERE IT GOES · HOW LONG. A four-column manifest is both more
   honest and faster to audit than the usual privacy prose wall. */

.ledger--data .ledger__cell {
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  vertical-align: top;
}

.ledger--data .ledger__cell:first-child {
  font-family: var(--font-display);
  font-size: var(--fs-base);
}

/* --- 7.4 .ledger--routes — contact.html's routing table ------------------ */
/* ROUTE · MAILBOX · RESPONSE WINDOW. Sits in an ink band: the routing table
   IS the page, and it correctly out-ranks the fallback form. */

.ledger--routes .ledger__cell {
  vertical-align: top;
}

/* .ledger__body, not just :first-child. Scoped only by the modifier this
   selector was (0,3,0) and beat `.ledger__head .ledger__cell` (0,2,0), so it
   was also sizing the HEAD's first cell — "ROUTE" set at lede while MAILBOX
   and RESPONSE WITHIN stayed micro tracked-caps, which reads as one column
   head shouting. Column heads are a set; they all take the same treatment. */
.ledger--routes .ledger__body .ledger__cell:first-child {
  font-family: var(--font-display);
  font-size: var(--fs-lede);
}

/* The mailbox is the row's ACTION, and it stands on its own line above the
   description rather than sitting inside a sentence — so WCAG 2.2's
   inline-link exception does not cover it and the 44px floor applies. At the
   inherited body size it was a 19px target, which is the wrong size for the
   single thing this page exists to make people click. The links inside the
   description below it stay inline and stay exempt. */
.route__mail {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--tap);
  font-family: var(--font-display);
  font-size: var(--fs-lede);
  line-height: var(--leading-lede);
}

/* --- 7.5 outbound links and category tags ------------------------------- */

.link--out::after {
  content: " \2197";   /* a character, not an icon — there is no icon set */
  text-decoration: none;
  font-size: 0.85em;
}

.tag {
  display: inline-block;
  padding: var(--sp-q) var(--sp-h);
  background-color: var(--surface);
  color: var(--teal-deep);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  white-space: nowrap;
}


/* ==========================================================================
   8. BLOCKS
   ========================================================================== */

/* --- 8.1 .contents — the anchor TOC ------------------------------------- */
/* The most literal colophon device on the site. .contents--ink is the dark
   instance and belongs to editorial-standards.html only; privacy.html uses
   the light one, so the two policy pages do not read as twins. */

.contents__list {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-h) var(--sp-2);
  list-style: none;
  counter-reset: contents;
}

@media (min-width: 48rem) {
  .contents__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Two columns: the accession numeral, then a stack. The stack matters —
   on index.html each entry carries its beat AND the latest headline pulled
   live from that title's feed, so the content column has to hold more than
   one line. With only a link present (the policy TOCs) it renders exactly
   as the old flex row did. */
.contents__item {
  counter-increment: contents;
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 0 var(--sp-h);
  border-block-end: var(--hair) solid var(--rule);
  padding-block-end: var(--sp-h);
}

.contents__item::before {
  content: counter(contents, decimal-leading-zero);
  grid-column: 1;
  grid-row: 1;
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-caps);
  color: var(--fg-mute);
  font-variant-numeric: tabular-nums;
  align-self: center;
}

.contents__link {
  grid-column: 2;
}

/* The live headline. Injected by feeds.js; absent with JS off, which is why
   the beat label above it is a complete entry on its own. */
.contents__latest {
  grid-column: 2;
  display: block;
  margin-block-start: var(--sp-q);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
}

.contents__latest a {
  color: var(--fg-mute);
}

.contents__latest a:hover {
  color: var(--link);
}

.contents__date {
  display: block;
  margin-block-start: var(--sp-q);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
  color: var(--fg-mute);
  font-variant-numeric: tabular-nums;
}

.contents__link {
  min-block-size: var(--tap);
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

.contents__link:hover {
  text-decoration-line: underline;
  text-decoration-thickness: var(--rule-section);
}

/* --- 8.2 .tally — the revised trust strip -------------------------------- */
/* Set as the FOOTER ROW of the register it counts, using the same column
   heads, so each figure is a column total of a table directly above it.
   Not a floating stat bar. */

.ledger__foot .tally__item {
  padding-block: var(--sp-1);
  padding-inline-end: var(--sp-1);
  border-block-start: var(--rule-section) solid var(--rule-strong);
  border-block-end: 0;
  /* A sticky thead is constrained by the TABLE, not the tbody, so once the
     whole register has scrolled past, the head parks at the table's bottom
     edge and its opaque ground paints straight over this row. Lifting the
     tally above it means the head disappears behind the totals instead —
     which is the correct reading order anyway. */
  position: relative;
  z-index: 2;
  background-color: var(--ground);
}

.tally--strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-2);
  align-items: baseline;
  list-style: none;   /* it is a <ul> on advertise.html */
}

.tally__figure {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-sub);
  line-height: var(--leading-sub);
  font-variant-numeric: tabular-nums;
}

.tally__label {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
  color: var(--fg-mute);
}

/* --- 8.3 .disclosure-note — the acquired-domain statement ---------------- */
/* Above the register on titles.html, not buried under it. */

.disclosure-note {
  background-color: var(--surface);
  color: var(--ink);
  border-inline-start: var(--rule-section) solid var(--teal-deep);
  padding: var(--sp-1) var(--sp-1h);
  max-inline-size: var(--measure);
}

.disclosure-note__head {
  display: block;
  margin-block-end: var(--sp-h);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
  color: var(--teal-deep);
}

.disclosure-note__body {
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
}

.disclosure-note__body > * + * {
  margin-block-start: var(--sp-h);
}

/* --- 8.4 .callout ------------------------------------------------------- */

.callout {
  border: var(--rule-section) solid var(--rule-strong);
  padding: var(--sp-1h);
  /* Same cap as .disclosure-note, and for the same reason: a callout holds
     prose, and prose holds the measure. Without this the box took the full
     band and set body copy at ~1060px. .callout--refusal opts out below —
     that one is a full-width list of short lines, not a paragraph. */
  max-inline-size: var(--measure);
}

/* base.css zeroes every element margin, so two paragraphs inside a callout
   run together into one block. The head already carries a bottom margin and
   adjacent margins collapse, so it needs no special case. */
.callout > * + * {
  margin-block-start: var(--sp-1);
}

.callout__head {
  display: block;
  margin-block-end: var(--sp-1);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
}

.callout__list {
  list-style: none;
}

.callout__list > li {
  padding-block: var(--sp-h);
  border-block-end: var(--hair) solid var(--rule);
}

.callout__list > li:last-child {
  border-block-end: 0;
}

/* WHAT WE DON'T ACCEPT. Prominent by WEIGHT, not by colour — this refusal
   list is a credibility asset, not fine print. */
.callout--refusal {
  border-color: var(--ink);
  background-color: var(--white);
  color: var(--ink);
  max-inline-size: none;
  padding: var(--sp-2);
}

.callout--refusal .callout__head {
  font-size: var(--fs-head);
  letter-spacing: 0;
  text-transform: none;
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  margin-inline-end: 0;
}

.callout--refusal .callout__list > li {
  font-size: var(--fs-lede);
  line-height: var(--leading-lede);
  padding-block: var(--sp-1);
}

.callout--warn {
  border-color: var(--teal-deep);
  background-color: var(--surface);
  color: var(--ink);
}

/* --- 8.5 .stepline — numbered procedure, no icons ------------------------ */

.stepline {
  list-style: none;
  counter-reset: stepline;
  display: grid;
  gap: var(--sp-1h);
}

@media (min-width: 48rem) {
  .stepline {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--sp-2);
  }
}

@media (min-width: 64rem) {
  .stepline--four {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.stepline__step {
  counter-increment: stepline;
  border-block-start: var(--rule-section) solid var(--rule-strong);
  padding-block-start: var(--sp-1);
}

.stepline__step::before {
  content: counter(stepline, decimal-leading-zero);
  display: block;
  margin-block-end: var(--sp-h);
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-sub);
  line-height: var(--leading-sub);
  font-variant-numeric: tabular-nums;
  color: var(--fg-accent);
}

.stepline__head {
  font-family: var(--font-display);
  font-size: var(--fs-lede);
  line-height: var(--leading-lede);
  margin-block-end: var(--sp-h);
}

.stepline__body {
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
}

/* --- 8.6 .faq — native <details>, no JS ---------------------------------- */

.faq {
  max-inline-size: var(--measure);
}

.faq__item {
  border-block-end: var(--hair) solid var(--rule);
}

.faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-1);
  min-block-size: var(--tap);
  padding-block: var(--sp-1);
  cursor: pointer;
  font-family: var(--font-display);
  font-size: var(--fs-lede);
  line-height: var(--leading-lede);
  list-style: none;
}

.faq__q::-webkit-details-marker {
  display: none;
}

.faq__q::after {
  content: "+";
  font-size: var(--fs-sub);
  color: var(--fg-accent);
  flex: 0 0 auto;
}

.faq__item[open] .faq__q::after {
  content: "\2212";
}

.faq__a {
  padding-block-end: var(--sp-1);
}

.faq__a > * + * {
  margin-block-start: var(--sp-1);
}

/* --- 8.7 .clause — §-numbered policy section ----------------------------- */

.clause {
  max-inline-size: var(--measure);
  padding-block-start: var(--sp-2);
}

/* A clause is not always one block of prose. §6 and §8 each run
   prose → .disclosure-note → prose, and base.css zeroes every element
   margin, so the bordered note sat flush against the paragraphs either
   side of it — which reads as the note belonging to the sentence above
   rather than standing on its own. .clause__head keeps its own larger
   margin; this only has to catch the siblings after it. */
.clause > * + * {
  margin-block-start: var(--sp-1h);
}

/* A clause's --sp-2 is the separator between one clause and the next. The
   FIRST clause in a band has no predecessor to separate from, so there it is
   a second helping of section opening laid on top of the band's own --sp-4:
   measured on editorial-standards.html at 1440, 158.4px between the contents
   band's edge and the §1 rule, against the 105.6px the band alone declares.
   Exactly what .pagehead--flush exists for, one component along, and the same
   removal — the band's padding is the whole opening.

   Opt-in per page rather than a `:first-child` rule, for the same reason
   .pagehead--flush is: privacy.html carries clauses too and has not been
   through this pass yet, and no page should move in a session that is not
   about it. */
.clause--flush {
  padding-block-start: 0;
}

.clause__head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-h);
  padding-block-end: var(--sp-h);
  border-block-end: var(--rule-section) solid var(--rule-strong);
  margin-block-end: var(--sp-1h);
}

.clause__num {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-caps);
  color: var(--fg-mute);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}

.clause__anchor {
  margin-inline-start: auto;
  min-block-size: var(--tap);
  display: inline-flex;
  align-items: center;
  font-size: var(--fs-fine);
  text-decoration: none;
  flex: 0 0 auto;
}

.clause__anchor:hover {
  text-decoration-line: underline;
}


/* ==========================================================================
   9. CONTROLS
   ========================================================================== */

/* --- 9.1 .btn ----------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-q);
  min-block-size: var(--tap);
  padding-inline: var(--sp-1h);
  padding-block: var(--sp-h);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  /* recentre the label against the trailing tracking space */
  text-indent: var(--track-caps);
  text-decoration: none;
  border: var(--rule-section) solid transparent;
  border-radius: var(--radius);
  color: var(--fg);
}

/* MICRO-INTERACTION (motion layer, §12 states the contract). One transition
   declaration owns every animated property on .btn — a second block adding
   `transition` for the press would silently discard the colour list, which
   is the same cancelled-out-property failure base.css §DISCIPLINE exists to
   prevent, one property along.
   The press is --press (1px) and nothing else: no scale, no shadow, no
   lift. A button that grows under the pointer is a card pretending to be a
   control, and there are no cards here. */
@media (prefers-reduced-motion: no-preference) {
  .btn {
    transition: background-color var(--dur) var(--ease),
                border-color var(--dur) var(--ease),
                color var(--dur) var(--ease),
                transform var(--dur-fast) var(--ease);
  }

  .btn:active {
    transform: translateY(var(--press));
  }
}

/* One of the two permitted gradient uses. See tokens.css for why this is the
   first half of the gradient rather than its full range. */
.btn--primary {
  background-image: var(--gradient-btn);
  color: var(--white);
  border-color: transparent;
}

.btn--primary:hover {
  background-image: none;
  background-color: var(--ink);
}

.btn--ghost {
  border-color: var(--teal);
  color: var(--teal);
}

.btn--ghost:hover {
  background-color: var(--surface);
}

/* for use inside ink bands */
.btn--ink {
  border-color: var(--on-ink);
  color: var(--on-ink);
}

.btn--ink:hover {
  background-color: var(--on-ink);
  color: var(--ink);
}

.btn--sm {
  min-block-size: var(--tap);
  padding-inline: var(--sp-1);
  font-size: var(--fs-micro);
}

.btn--block {
  inline-size: 100%;
}

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
}

/* --- 9.2 .form / .field / .control --------------------------------------- */

.form {
  max-inline-size: var(--measure);
}

/* base.css zeroes every element margin, and `.field + .field` only covers
   field-to-field. Everything else a form legitimately holds — a submit
   paragraph, a secondary action, a status line — landed flush against
   whatever preceded it, so the consent box and the submit button touched.
   The form supplies the rhythm for all of its children; the specific rules
   below restate the same value where they need to win, not a different one. */
.form > * + * {
  margin-block-start: var(--sp-1h);
}

/* A .js-only wrapper is a grouping element, not a step in the form, so its
   own children need the rhythm the wrapper is absorbing. One line rather
   than one and a half: these belong together as a single fallback block. */
.form .js-only > * + * {
  margin-block-start: var(--sp-1);
}

.field + .field {
  margin-block-start: var(--sp-1h);
}

.field__label {
  display: block;
  margin-block-end: var(--sp-q);
  font-family: var(--font-display);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
}

.field__hint {
  display: block;
  margin-block-start: var(--sp-q);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
}

.field__error {
  display: block;
  margin-block-start: var(--sp-q);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--ink);
  border-inline-start: var(--rule-section) solid var(--teal-deep);
  padding-inline-start: var(--sp-h);
}

/* No rounded box anywhere: a single 2px bottom rule. */
.control {
  inline-size: 100%;
  min-block-size: var(--tap);
  padding-block: var(--sp-h);
  background-color: transparent;
  color: var(--fg);
  border-block-end: var(--rule-section) solid var(--rule-strong);
}

.control:focus-visible {
  outline-offset: var(--hair);
}

/* The one exception: a multi-line field needs a bounded box or its text runs
   off the rule. Still square, still no shadow. */
.control--area {
  min-block-size: calc(var(--lh) * 5);
  padding: var(--sp-h);
  border: var(--rule-section) solid var(--rule-strong);
}

.control--select {
  padding-inline-end: var(--sp-1h);
}

.control[aria-invalid="true"],
.is-invalid .control {
  border-color: var(--ink);
  border-block-end-width: var(--rule-sig);
}

/* --- 9.3 checkbox + consent ---------------------------------------------- */

.checkbox {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-h);
  min-block-size: var(--tap);
  padding-block: var(--sp-q);
  cursor: pointer;
}

.checkbox__input {
  flex: 0 0 auto;
  inline-size: var(--sp-1);
  block-size: var(--sp-1);
  margin-block-start: var(--sp-q);
  accent-color: var(--teal);
  appearance: auto;
  cursor: pointer;
}

.checkbox__text {
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
}

.consent {
  margin-block-start: var(--sp-1h);
  padding-block-start: var(--sp-1);
  border-block-start: var(--hair) solid var(--rule);
}

/* --- 9.4 .filterbar — CSS-only category filter (titles.html) -------------- */
/* REQUIRED DOM. .filterbar is the SCOPE, not a bar wrapped around the
   labels — the register must be a sibling of the radios or `~` cannot reach
   it, and each label must directly follow its own radio or `+` cannot reach
   it. So the radios, their labels, and the register are all children of one
   element, interleaved:
       <div class="filterbar">
         <input class="filterbar__input" type="radio" name="cat"
                id="cat-all" value="all" checked>
         <label class="filterbar__label" for="cat-all">All</label>
         <input class="filterbar__input" type="radio" name="cat"
                id="cat-buying" value="buying">
         <label class="filterbar__label" for="cat-buying">Buying…</label>
         …one input+label pair per cluster…
         <p class="filterbar__state" data-cat="buying"> … </p>
         …one state line per cluster…
         <table class="ledger ledger--titles"> … </table>
       </div>
   The radios are clipped, so the labels flow as a bar and the register drops
   to its own line via the 100% flex-basis rule below. With JS off — and with
   "all" checked — the full list is visible: the filter only ever subtracts.

   RESOLVED IN THE titles.html SESSION (2026-08-09). This block previously
   enumerated ELEVEN category slugs — beauty, packaging, curiosities, faith
   and the rest. Every one of them was a PLACEHOLDER category that the
   research pass in docs/site-research-table.md disproved: eleven of the
   fifteen were wrong, four badly (gottoglow is personal technology, not
   beauty; ask-kabbalist publishes no faith content in a 93-entry sample).
   None of those eleven slugs exists in docs/portfolio.json any more, so
   every selector below was dead CSS matching nothing.

   Replaced with the SIX subject clusters actually shipped on titles.html.
   Why six and not fifteen: portfolio.json now carries fifteen DISTINCT
   evidenced beats, and filtering fifteen rows by fifteen categories gives
   fifteen groups of one — a filter that can only ever hide fourteen rows is
   not a filter. The research's own conclusion is that the fifteen are
   "fifteen angles on one broad subject", so the clusters are the honest
   grouping and each row still carries its own precise beat.

   Never write "8 subject areas" anywhere — that count came from PROMPTS.md's
   draft trust-strip copy and has never matched the data. */

.filterbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-h);
  margin-block-start: var(--sp-1);
}

/* anything that is not a control breaks to its own full-width line */
.filterbar > .ledger,
.filterbar > .disclosure-note,
.filterbar > .filterbar__break {
  flex: 1 0 100%;
  margin-block-start: var(--sp-h);
}

.filterbar__input {
  position: absolute;
  inline-size: var(--hair);
  block-size: var(--hair);
  overflow: hidden;
  clip-path: inset(50%);
}

.filterbar__label {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--tap);
  padding-inline: var(--sp-1);
  border: var(--rule-section) solid var(--rule);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  text-indent: var(--track-caps);
  color: var(--teal-deep);
  cursor: pointer;
}

.filterbar__input:checked + .filterbar__label {
  background-color: var(--ink);
  border-color: var(--ink);
  color: var(--on-ink);
}

.filterbar__input:focus-visible + .filterbar__label {
  outline: var(--rule-section) solid var(--focus-c);
  outline-offset: var(--rule-section);
}

/* The six clusters. A group head carries its OWN data-cat so it survives its
   own filter and disappears under the other five. The tally row deliberately
   carries none: it totals the whole register, so it is correct only in the
   unfiltered view and hides itself the moment a subset is selected. */
.filterbar__input[value="buying"]:checked    ~ .ledger .ledger__row:not([data-cat="buying"]),
.filterbar__input[value="setup"]:checked     ~ .ledger .ledger__row:not([data-cat="setup"]),
.filterbar__input[value="repair"]:checked    ~ .ledger .ledger__row:not([data-cat="repair"]),
.filterbar__input[value="scams"]:checked     ~ .ledger .ledger__row:not([data-cat="scams"]),
.filterbar__input[value="platforms"]:checked ~ .ledger .ledger__row:not([data-cat="platforms"]),
.filterbar__input[value="work"]:checked      ~ .ledger .ledger__row:not([data-cat="work"]) {
  display: none;
}

/* A filtered register is a subset, and the caption must say which subset —
   otherwise a reader who lands mid-page reads four rows as the whole
   network. One line per cluster, revealed by the same :checked state that
   does the filtering, so the statement cannot drift out of sync with what
   is actually on screen. */
.filterbar__state {
  display: none;
  flex: 1 0 100%;
  margin-block-start: var(--sp-h);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
}

.filterbar__input[value="buying"]:checked    ~ .filterbar__state[data-cat="buying"],
.filterbar__input[value="setup"]:checked     ~ .filterbar__state[data-cat="setup"],
.filterbar__input[value="repair"]:checked    ~ .filterbar__state[data-cat="repair"],
.filterbar__input[value="scams"]:checked     ~ .filterbar__state[data-cat="scams"],
.filterbar__input[value="platforms"]:checked ~ .filterbar__state[data-cat="platforms"],
.filterbar__input[value="work"]:checked      ~ .filterbar__state[data-cat="work"] {
  display: block;
}


/* ==========================================================================
   10. advertise.html ONLY
   A cart collecting "1× Guest Post" is worthless. Every block below exists
   to make the submitted order priceable without a clarifying email.
   ========================================================================== */

/* --- 10.1 .cart-tab — persistent affordance ------------------------------ */
/* An ink pill inside the white masthead. It has to be an ink ground because
   the live total is the one cyan text instance on the page, and cyan on
   white is 1.78:1. */

.cart-tab {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-h);
  min-block-size: var(--tap);
  padding-inline: var(--sp-1);
  background-color: var(--ink);
  color: var(--on-ink);
  --focus-c: var(--cyan);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  text-indent: var(--track-caps);
  text-decoration: none;
  flex: 0 0 auto;
}

.cart-tab__count {
  font-variant-numeric: tabular-nums;
}

.cart-tab__total {
  color: var(--cyan);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 47.999rem) {
  .cart-tab--sticky {
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: var(--z-cart-tab);
    justify-content: space-between;
    padding-block: var(--sp-h);
    border-block-start: var(--rule-section) solid var(--cyan);
  }
}

/* --- 10.2 .cart — the drawer -------------------------------------------- */
/* <dialog> supplies the focus trap, Esc-to-close and focus return. The
   .cart__scrim rule covers the non-dialog fallback. */

.cart {
  position: fixed;
  inset-block: 0;
  inset-inline-end: 0;
  inset-inline-start: auto;
  z-index: var(--z-drawer);
  inline-size: min(var(--cart-w), 100%);
  max-inline-size: 100%;
  max-block-size: 100%;
  block-size: 100%;
  margin: 0;
  padding: 0;
  /* ONE scroll column, not a flex sandwich. As a three-part flex column the
     item list was `flex: 1 1 auto` between a fixed totals block and a fixed
     actions block; once the contact fields were added, actions grew past the
     viewport and crushed the list to ZERO height — the buyer could not see
     what they were ordering. Measured: list 0px tall holding 357px of
     content. A single scrolling column cannot fail that way at any content
     length; the head stays pinned instead. */
  display: block;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Declare the GROUND, don't set a raw colour. base.css gives headings
     `color: var(--fg)` via an element selector, which beats any `color`
     inherited from an ancestor class — so a surface that sets `color:`
     directly renders its own headings in the wrong tone. That is what put
     --ink text on the --ink drawer head (1:1, invisible). Every dark surface
     in this system reassigns the ground tokens; the cart now does too. */
  --ground: var(--white);
  --fg: var(--ink);
  --fg-mute: var(--teal-deep);
  --rule: var(--line);
  --rule-strong: var(--teal-deep);
  --focus-c: var(--teal);
  background-color: var(--ground);
  color: var(--fg);
  border: 0;
  border-inline-start: var(--rule-section) solid var(--ink);
}

.cart:not([open]) {
  display: none;
}

.cart::backdrop {
  background-color: var(--scrim);
}

.cart__scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-scrim);
  background-color: var(--scrim);
}

.cart__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-1h);
  /* ink ground — full token reassignment, so the <h2> inside inherits it */
  --ground: var(--ink);
  --fg: var(--on-ink);
  --fg-mute: var(--on-ink);
  --rule: var(--rule-on-ink);
  --rule-strong: var(--rule-on-ink-strong);
  --focus-c: var(--cyan);
  background-color: var(--ground);
  color: var(--fg);
  position: sticky;
  inset-block-start: 0;
  z-index: 1;
}

.cart__title {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-fine);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
}

.cart__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--tap);
  min-block-size: var(--tap);
  color: var(--on-ink);
}

.cart__list {
  padding-inline: var(--sp-1h);
  list-style: none;
}

.cart__line {
  padding-block: var(--sp-1);
  border-block-end: var(--hair) solid var(--line);
}

.cart__line-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-h);
}

.cart__line-name {
  font-family: var(--font-display);
  font-size: var(--fs-lede);
  line-height: var(--leading-lede);
}

.cart__line-total {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}

/* The captured configuration rendered back as a <dl> — sites, target URLs,
   anchors, article source. The buyer SEES the order is unambiguous before
   sending. This block is the whole reason the cart exists. */
.cart__line-meta {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--sp-q) var(--sp-h);
  margin-block-start: var(--sp-h);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
}

.cart__line-meta dt {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--teal-deep);
}

.cart__line-meta dd {
  word-break: break-word;
}

.cart__line-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-h);
  margin-block-start: var(--sp-h);
}

.cart__empty {
  padding-block: var(--sp-2);
  color: var(--teal-deep);
  font-size: var(--fs-fine);
}

.cart__totals {
  padding: var(--sp-1) var(--sp-1h);
  border-block-start: var(--rule-section) solid var(--ink);
  background-color: var(--surface);
}

.cart__total-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-1);
  padding-block: var(--sp-q);
  font-size: var(--fs-fine);
  font-variant-numeric: tabular-nums;
}

.cart__total-row--grand {
  margin-block-start: var(--sp-h);
  padding-block-start: var(--sp-h);
  border-block-start: var(--hair) solid var(--line);
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-sub);
}

.cart__disclaimer {
  margin-block-start: var(--sp-q);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  color: var(--teal-deep);
}

.cart__actions {
  display: grid;
  gap: var(--sp-h);
  padding: var(--sp-1) var(--sp-1h) var(--sp-1h);
}

/* --- 10.3 .config — the configuration panel ------------------------------ */
/* Nothing enters the cart unconfigured. The panel's submit stays disabled
   until every required field for that service validates. */

.config {
  inline-size: min(var(--config-w), 100%);
  max-inline-size: 100%;
  max-block-size: 100%;
  margin: auto;
  padding: 0;
  display: flex;
  flex-direction: column;
  background-color: var(--white);
  color: var(--ink);
  border: var(--rule-section) solid var(--ink);
}

/* The <form> is the dialog's ONLY child — head, body and foot live inside
   it. Making .config a flex column therefore did nothing to them: the form
   stayed display:block, stacked them at full content height, and pushed the
   submit button off-screen. The form has to BE the column. */
.config__form {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-block-size: 0;
  max-block-size: 100%;
}

.config:not([open]) {
  display: none;
}

.config::backdrop {
  background-color: var(--scrim);
}

.config__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-1h);
  border-block-end: var(--rule-section) solid var(--ink);
  flex: 0 0 auto;
}

.config__title {
  font-family: var(--font-display);
  font-size: var(--fs-sub);
  line-height: var(--leading-sub);
}

.config__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--tap);
  min-block-size: var(--tap);
  flex: 0 0 auto;
}

.config__body {
  flex: 1 1 auto;
  /* Without this the panel silently breaks. A flex item's automatic minimum
     size is its CONTENT size, so `flex: 1 1 auto` + `overflow-y: auto` does
     not scroll — the body grows to its full content height and pushes
     .config__foot, and therefore the "Add to order" button, off-screen.
     Measured before the fix: 2857px of body inside a 935px dialog, submit
     unreachable. min-block-size: 0 lets it shrink and scroll instead. */
  min-block-size: 0;
  overflow-y: auto;
  padding: var(--sp-1h);
}

.config__row + .config__row {
  margin-block-start: var(--sp-1h);
  padding-block-start: var(--sp-1h);
  border-block-start: var(--hair) solid var(--line);
}

.config__row-head {
  display: block;
  margin-block-end: var(--sp-h);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
  color: var(--teal-deep);
}

/* A URL and its anchor text belong side by side — they are one decision, and
   splitting them across two full-width rows is how buyers end up submitting
   a target with no anchor. Collapses to stacked below 48rem. */
.config__row-pair {
  display: grid;
  gap: var(--sp-h) var(--sp-1);
  align-items: end;
}

@media (min-width: 48rem) {
  .config__row-pair {
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  }
}

.config__row-pair + .config__row-pair {
  margin-block-start: var(--sp-1);
}

.config__row-pair .field + .field {
  margin-block-start: 0;
}

.config__foot {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-h);
  padding: var(--sp-1) var(--sp-1h);
  border-block-start: var(--rule-section) solid var(--ink);
  background-color: var(--surface);
}

/* live repricing, aria-live="polite" */
.config__summary {
  font-size: var(--fs-fine);
  color: var(--teal-deep);
}

.config__price {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-sub);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* --- 10.4 .stepper ------------------------------------------------------- */
/* Real 44px targets over a native number input, so keyboard and JS-off both
   work. Reprices on `input`. */

.stepper {
  display: inline-flex;
  align-items: stretch;
  border: var(--rule-section) solid var(--rule-strong);
}

.stepper__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--tap);
  min-block-size: var(--tap);
  font-family: var(--font-display);
  font-size: var(--fs-lede);
  color: var(--fg);
}

.stepper__btn:hover {
  background-color: var(--surface);
}

.stepper__field {
  inline-size: var(--stepper-w);
  min-block-size: var(--tap);
  text-align: center;
  font-variant-numeric: tabular-nums;
  border-inline: var(--hair) solid var(--rule);
}

/* --- 10.5 .chipset — the site picker ------------------------------------ */
/* 15 checkbox-backed chips grouped by category, select-all and
   select-by-category. Each .chip is a <label> wrapping a visually-hidden
   checkbox, so the whole picker is fully functional with JS off — which is
   what lets this entire page degrade to a usable form. */

.chipset__group + .chipset__group {
  margin-block-start: var(--sp-1h);
}

.chipset__group-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-h);
  padding-block-end: var(--sp-h);
  margin-block-end: var(--sp-h);
  border-block-end: var(--hair) solid var(--rule);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--fg-mute);
}

.chipset__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-h);
  list-style: none;
}

.chipset__bulk {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-h);
  margin-block-end: var(--sp-1h);
}

.chip {
  display: inline-flex;
  cursor: pointer;
}

.chip__input {
  position: absolute;
  inline-size: var(--hair);
  block-size: var(--hair);
  overflow: hidden;
  clip-path: inset(50%);
}

.chip__face {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--tap);
  padding-inline: var(--sp-1);
  border: var(--rule-section) solid var(--rule);
  font-size: var(--fs-fine);
  color: var(--fg);
}

@media (prefers-reduced-motion: no-preference) {
  .chip__face {
    transition: background-color var(--dur) var(--ease),
                border-color var(--dur) var(--ease),
                color var(--dur) var(--ease);
  }
}

.chip__input:checked + .chip__face {
  background-color: var(--ink);
  border-color: var(--ink);
  color: var(--on-ink);
}

.chip__input:focus-visible + .chip__face {
  outline: var(--rule-section) solid var(--focus-c);
  outline-offset: var(--rule-section);
}

/* --- 10.6 .pack — network packages -------------------------------------- */
/* NETWORK 15 occupies the WHOLE of ink band ② as a single full-width row.
   Dominance comes from ground, scale and isolation. There is no featured
   card, no ribbon, no scale transform, no "Most Popular" — a badge is what
   you reach for when the layout is not doing the work. */

/* `start`, not `end`. In v1 the copy column was a single <h2> and ending the
   two columns on a shared baseline read as one line of type. v2's copy column
   is a heading plus a lede, a channels register and a button, so `end` parked
   the $180 figure at the BOTTOM of a four-element column with ~200px of dead
   space above it and the heading floating alone at the top. Aligning to the
   start puts the figure and the heading on the same line, which is the
   relationship the block is making. */
.pack {
  display: grid;
  gap: var(--sp-1h);
  align-items: start;
}

@media (min-width: 64rem) {
  .pack {
    grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
    gap: var(--sp-2);
  }
}

.pack__name {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-fine);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
  color: var(--fg-mute);
}

.pack__figure {
  display: block;
  margin-block-start: var(--sp-h);
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-masthead);
  line-height: var(--leading-masthead);
  font-variant-numeric: tabular-nums;
}

.pack__sites {
  display: block;
  margin-block-start: var(--sp-h);
  font-size: var(--fs-fine);
  color: var(--fg-mute);
}

.pack__copy {
  max-inline-size: var(--measure);
  font-size: var(--fs-lede);
  line-height: var(--leading-lede);
}

.pack__action {
  margin-block-start: var(--sp-1h);
}

/* --- 10.7 .surcharge — restricted niches -------------------------------- */
/* The reason is shown inline, never footnoted. */

.surcharge {
  max-inline-size: var(--measure);
}

.surcharge__reason {
  margin-block-start: var(--sp-h);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
}

/* --- 10.8 .orderref / .orderdump / .copybtn ----------------------------- */
/* Path 3 is PERMANENT, not an error fallback: mailto silently truncates long
   orders in some clients, and a truncated order is a lost order. */

.orderref {
  display: inline-block;
  padding: var(--sp-q) var(--sp-h);
  border: var(--hair) solid var(--rule-strong);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.orderdump {
  max-block-size: calc(var(--lh) * 14);
  overflow: auto;
  padding: var(--sp-1);
  background-color: var(--surface);
  color: var(--ink);
  border: var(--hair) solid var(--line);
  white-space: pre-wrap;
  word-break: break-word;
  user-select: all;
}

.copybtn {
  font-variant-numeric: tabular-nums;
}

/* shown when the assembled mailto exceeds 1800 chars */
.mailto-warning {
  margin-block: var(--sp-h);
}

/* --- 10.9 .badge--sponsored --------------------------------------------- */
/* Disclosed links are why placements hold value through core updates. Stated
   as a strength. Must match editorial-standards.html word for word. */

.badge--sponsored {
  display: inline-block;
  padding: var(--sp-q) var(--sp-h);
  background-color: var(--surface);
  color: var(--teal-deep);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  white-space: nowrap;
}


/* ==========================================================================
   11. STATE UTILITIES
   ========================================================================== */

/* .is-open is always qualified by its block (.nav.is-open above) — there is
   no bare .is-open rule, because a global display override is exactly the
   kind of utility that silently cancels a component's own layout. */

.is-active {
  font-weight: var(--weight-bold);
}

.is-busy {
  cursor: progress;
  opacity: 0.6;
}

/* Build-time marker on every unfilled [[PLACEHOLDER]]. Makes what is still
   missing visible at a glance rather than blending into finished copy, and
   keeps long tokens from blowing out a register column. Delete this class
   from the pages — not from here — once docs/fill-in-checklist.md is clear. */
.placeholder {
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  color: var(--fg-mute);
  word-break: break-all;
  white-space: normal;
}

/* .is-invalid is consumed by .control above.
   .no-js / .js-only live in base.css — the JS-off path is the default
   rendering, never an afterthought. */


/* ==========================================================================
   11. THE STOREFRONT — advertise.html's guided ordering flow
   Added 2026-08-09 on the owner's instruction: no price is shown until the
   buyer has chosen at least one website, and the flow should read like an
   ordinary online shop rather than a rate card.

   WHAT WAS TAKEN FROM THE BIG RETAILERS AND WHAT WAS NOT.
   Taken: you always know which step you are on, what is in your basket, what
   it costs, and what happens next. Those four are why Amazon converts, and
   none of them is a visual style.
   Not taken: the drop-shadowed rounded card grid. §7 of docs/design-system.md
   rejects it for this property, and it would not survive the AI-template
   critique the whole system was built against. The register IS the product
   list here — it just gained a price that only appears once it can be true.
   ========================================================================== */

/* --- 11.1 .step — the numbered stage header ----------------------------- */
/* Three states, and the locked one has to be obvious without relying on
   colour alone: it carries the word "Locked" in text, not just a tone. */

.step {
  display: flex;
  align-items: baseline;
  gap: var(--sp-1);
  margin-block-end: var(--sp-1);
}

.step__num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--sp-2);
  block-size: var(--sp-2);
  flex: 0 0 auto;
  background-color: var(--ink);
  color: var(--white);
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-sub);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.step--locked .step__num {
  background-color: transparent;
  color: var(--teal-deep);
  box-shadow: inset 0 0 0 var(--rule-section) var(--rule);
}

.step--done .step__num {
  background-color: var(--teal);
}

.step__text {
  flex: 1 1 auto;
  min-inline-size: 0;
}

.step__state {
  display: block;
  margin-block-start: var(--sp-q);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
  color: var(--teal-deep);
}

/* The whole of step 2 before any website is chosen. Not display:none —
   the buyer must be able to SEE what is behind the gate, or the page looks
   like it is hiding something. It is dimmed and not operable, and the
   reason is stated in text directly above it. */
.gated {
  opacity: 0.45;
  filter: grayscale(1);
  pointer-events: none;
  user-select: none;
}

@media (prefers-reduced-motion: no-preference) {
  .gated,
  .shop__price {
    transition: opacity var(--dur) var(--ease), filter var(--dur) var(--ease);
  }
}

/* --- 11.2 .pickbar — the running "you picked N websites" line ----------- */
/* Sticky under the masthead through step 1 only. The count is the single
   number that unlocks everything else, so it never scrolls out of sight
   while the buyer is choosing. */

.pickbar {
  position: sticky;
  inset-block-start: var(--masthead-h);
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-h) var(--sp-1);
  margin-block: var(--sp-1);
  padding: var(--sp-1);
  background-color: var(--ink);
  color: var(--on-ink);
}

.pickbar__count {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-sub);
  line-height: var(--leading-sub);
  font-variant-numeric: tabular-nums;
  flex: 1 1 auto;
  min-inline-size: 0;
}

.pickbar__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-h);
  flex: 0 0 auto;
}

/* --- 11.3 .shop — the product register ---------------------------------- */
/* Same ledger bones as .ledger--rate, plus a price cell that starts empty. */

.shop {
  list-style: none;
}

/* --sp-1, not --sp-1h. The comment above says "same ledger bones", and it
   was the only register on the page that did not have them: .ledger__cell
   and .plainbox__row both sit at --sp-1, and this sat half a line above
   them. Twenty-one rows carried 1663px of padding — 11.5% of the page —
   and the seven extras rows were 143px tall around 62px of content. Bringing
   it to the value the other two registers already use takes 554px out of
   the page and makes the three registers read as one grammar. */
.shop__row {
  display: grid;
  gap: var(--sp-h) var(--sp-1);
  padding-block: var(--sp-1);
  border-block-end: var(--hair) solid var(--rule);
  grid-template-columns: 1fr;
}

@media (min-width: 48rem) {
  .shop__row {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
  }
}

.shop__name {
  font-family: var(--font-display);
  font-size: var(--fs-sub);
  line-height: var(--leading-sub);
}

/* The plain-English line under the product name. This is the one that has to
   make sense to somebody who has never bought a link in their life, so it is
   set at reading size rather than as fine print. */
.shop__plain {
  display: block;
  max-inline-size: var(--measure);
  margin-block-start: var(--sp-q);
  font-size: var(--fs-base);
  line-height: var(--leading-base);
}

.shop__gets {
  display: block;
  max-inline-size: var(--measure);
  margin-block-start: var(--sp-h);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
}

.shop__buy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-h);
}

@media (min-width: 48rem) {
  .shop__buy {
    align-items: flex-end;
    text-align: end;
    min-inline-size: var(--gutter-note);
  }
}

/* --- 11.4 .shop__price — the gated number ------------------------------- */

.shop__price {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-head);
  line-height: var(--leading-head);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Before a website is chosen. Reads as a state, not as a missing value. */
.shop__price--locked {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  line-height: var(--leading-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
  color: var(--teal-deep);
  white-space: normal;
}

/* The arithmetic, shown rather than hidden: "$15 x 3 websites". A buyer who
   can see how the number was reached does not email to ask how it was
   reached — which is this page's whole success metric. */
.shop__working {
  display: block;
  font-family: var(--font-body);
  font-weight: var(--weight-body);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
  white-space: normal;
}

/* --- 11.5 .storebar — the persistent basket ----------------------------- */
/* Fixed to the bottom edge, not the top: the masthead already owns the top,
   and on a phone the thumb is at the bottom. Hidden until the basket has
   something in it, so it never covers content it has no business covering. */

.storebar {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: var(--z-cart-tab);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-h) var(--sp-1);
  padding: var(--sp-h) var(--sp-1);
  background-color: var(--ink);
  color: var(--on-ink);
  border-block-start: var(--rule-section) solid var(--cyan);
}

@media (min-width: 48rem) {
  .storebar {
    padding-inline: var(--sp-2);
  }
}

.storebar[hidden] {
  display: none;
}

.storebar__summary {
  flex: 1 1 auto;
  min-inline-size: 0;
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
}

.storebar__total {
  display: block;
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-sub);
  line-height: var(--leading-sub);
  font-variant-numeric: tabular-nums;
}

/* The bar is fixed, so the last section needs somewhere to end. */
.has-storebar .colophon {
  padding-block-end: var(--sp-6);
}

/* --- 11.6 .plainbox — "what these words mean" --------------------------- */
/* An ordinary buyer does not know what an anchor is, and pretending
   otherwise is how orders arrive incomplete. */

.plainbox {
  max-inline-size: var(--measure);
}

.plainbox__row {
  padding-block: var(--sp-1);
  border-block-end: var(--hair) solid var(--rule);
}

.plainbox__row:last-child {
  border-block-end: 0;
}

.plainbox__term {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-lede);
  line-height: var(--leading-lede);
}

.plainbox__def {
  display: block;
  margin-block-start: var(--sp-q);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
}


/* ==========================================================================
   12. MOTION — the shared entrance layer
   Added 2026-08-10 (docs/fix-pass-plan.md session 8). Built once here,
   applied to index.html as the reference page.

   THE CONTRACT — three gates, all of which must open before anything below
   renders. Nothing in this section is decoration that degrades; it is
   enhancement that is absent by default.

     1. The PAGE opts in:   <body data-motion>
        Without the attribute site.js does nothing and the page is exactly
        the document it was before this section existed. This is what keeps
        the layer applicable one page at a time.
     2. The ENGINE is available: site.js sets html.js-motion only after it
        has an IntersectionObserver and has registered every band, inside a
        try/catch that removes the class again on any throw. A page that
        hides content it can never reveal is the one unacceptable failure
        mode here, so the hidden state cannot exist without a live observer.
     3. The READER has not asked for less: the whole section sits inside
        `prefers-reduced-motion: no-preference`, and site.js checks the same
        query before it ever adds the class.

   THE REDUCED-MOTION FALLBACK IS THE STATIC DOCUMENT, NOT A FAST ONE.
   Under `reduce` this section does not apply at all: bands are never
   hidden, no transition is scheduled, the motif is painted whole and the
   signature rule is at full width from first paint. base.css §9 clamps
   durations to 0.01ms as a site-wide backstop for anything that slips
   through — this layer never relies on it, because a 0.01ms fade is still a
   thing that happened, and an element that must be un-hidden by a
   near-instant transition is one dropped frame away from invisible.

   WHAT THE LAYER DELIBERATELY DOES NOT DO
   · No per-child stagger, no cascading rows. The band is the unit of
     arrival because the band is the unit of composition (§1); fifteen
     register rows arriving one after another would animate the data.
   · No reversal on scroll-up. Each band reveals once and is unobserved —
     re-hiding read content is the reason scroll animation has a bad name.
   · No parallax, no scale, no shadow, no new colour, no new gradient
     instance, no fourth motif placement. The motion layer spends nothing
     from the cyan, gradient or motif budgets: it animates only what the
     page had already earned the right to paint.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {

  /* --- 12.1 the band entrance ------------------------------------------- */
  /* One gesture: half a line of rise and a fade, heavily decelerated so the
     band settles rather than slides. site.js writes data-reveal="band" on
     every .band of an opted-in page, and adds .is-revealed in the SAME
     frame for anything already on screen — so the hidden state above the
     fold is set and cleared before a single paint, and there is no flash. */

  .js-motion [data-reveal="band"] {
    opacity: 0;
    transform: translateY(var(--reveal-rise));
    transition: opacity var(--dur-slow) var(--ease-entrance),
                transform var(--dur-slow) var(--ease-entrance);
  }

  /* `none`, not translateY(0). A transform that survives the entrance makes
     the band a containing block for the rest of the session, and
     .ledger--titles parks a `position: sticky` head inside one of these.
     `none` interpolates against a transform list as the identity matrix, so
     the transition still runs — and ends with the property genuinely gone. */
  .js-motion [data-reveal="band"].is-revealed {
    opacity: 1;
    transform: none;
  }

  /* --- 12.2 the motif draws ---------------------------------------------- */
  /* The signature element earns its place in the motion system the same way
     it earns it in the static one: as structure, not ornament. When a band
     carrying the marker arrives, its three strokes grow up out of the
     baseline and the chip lands last — many channels resolving to one
     point, which is what the mark means (CLAUDE.md, CHANNEL MOTIF).

     This is CSS only and adds NO placement: it animates the background-size
     of the four layers §6.1 already paints, on the marker the page already
     has. The three-per-page budget is untouched, and on an ink ground the
     one cyan chip is the same one chip.

     .channels (Implementation C) is deliberately excluded — it carries the
     arrival cursor, which is data, and data does not make an entrance. */

  .js-motion [data-reveal="band"] .motif::before {
    background-size:
      var(--motif-hair) 0,
      var(--motif-hair) 0,
      var(--motif-hair) 0,
      0 0;
    transition: background-size var(--dur-slow) var(--ease-entrance);
  }

  .js-motion [data-reveal="band"].is-revealed .motif::before {
    background-size:
      var(--motif-hair) 100%,
      var(--motif-hair) 100%,
      var(--motif-hair) 100%,
      var(--motif-chip) var(--motif-chip);
  }

  /* --- 12.3 the signature rule wipes ------------------------------------- */
  /* .rule--sig is the page's one gradient rule and its one structural
     pivot; drawing it from the inline-start edge is the layer's single
     horizontal gesture, and it reads as the page being set rather than as
     an effect. No second gradient instance: this is the same --gradient
     layer §5 declares, revealed by size. background-repeat is set here
     rather than in §5 because it only matters while the width is short of
     100%, which only ever happens inside this block. */

  .js-motion [data-reveal="band"] .rule--sig {
    background-repeat: no-repeat;
    background-position: left center;
    background-size: 0 100%;
    transition: background-size var(--dur-slow) var(--ease-entrance);
  }

  .js-motion [data-reveal="band"].is-revealed .rule--sig {
    background-size: 100% 100%;
  }

  /* --- 12.4 the channel rules draw and wipe together ---------------------
     Extended 2026-08-10 (session 9, titles.html). 12.2 and 12.3 were written
     against the two implementations index.html happens to use — .motif for
     the marker, .rule--sig for the pivot. Implementation B (§6.2) is BOTH of
     those things in one element, and on titles.html it is the only one the
     page has: `.rule--channel--sig` is that page's single signature rule and
     its first motif instance, which its own markup comment already states as
     a design fact. Without this block §12 would skip the page's signature
     elements over a class name, so this is the same two gestures reaching
     the same two things, not a third gesture.

     One transition on one property does both, because on this element they
     are one element: layer 1 is the horizontal rule and wipes from the
     inline-start, layer 2 is the comb and draws up out of the baseline,
     layer 3 is the chip and scales in on the last stroke. No position, no
     repeat and no image is touched — §6.2 already sets all three, and the
     revealed state below is byte-for-byte the background-size §6.2 declares.
     Nothing is spent from the motif, gradient or cyan budgets. */

  .js-motion [data-reveal="band"] .rule--channel {
    background-size:
      0 var(--hair),
      calc(var(--motif-gap) * var(--motif-n)) 0,
      0 0;
    transition: background-size var(--dur-slow) var(--ease-entrance);
  }

  .js-motion [data-reveal="band"].is-revealed .rule--channel {
    background-size:
      100% var(--hair),
      calc(var(--motif-gap) * var(--motif-n)) 100%,
      var(--motif-chip) var(--motif-chip);
  }

  /* Same selector weight as the pair above, so source order is what makes
     these win — they must stay below it. The only difference is layer 1:
     the signature form's first layer is the --rule-sig gradient bar, not the
     --hair rule, and a wipe that ended at the wrong height would resize the
     rule rather than reveal it. */
  .js-motion [data-reveal="band"] .rule--channel--sig {
    background-size:
      0 var(--rule-sig),
      calc(var(--motif-gap) * var(--motif-n)) 0,
      0 0;
  }

  .js-motion [data-reveal="band"].is-revealed .rule--channel--sig {
    background-size:
      100% var(--rule-sig),
      calc(var(--motif-gap) * var(--motif-n)) 100%,
      var(--motif-chip) var(--motif-chip);
  }
}

/* ==========================================================================
   13. ADVERTISE v2 — the browse/configure split
   Implements docs/design-system.md §5.9. Added 2026-08-10.

   §11 (the v1 storefront) put the catalogue on the page: 4 services, 7
   upgrades, 6 placements and 5 packages, flat, with the ordering steps as
   scrolling bands. v2 keeps §11's two real wins — nothing enters the order
   unconfigured, and the running total is never a surprise — and moves the
   configuring itself off the page into a drawer.

   WHY THIS IS NOT A CARD GRID, despite containing a thing called .sitecard.
   §7 of docs/design-system.md rejects the drop-shadowed rounded card, and
   that rejection stands. .sitecard is a BUTTON that happens to be
   rectangular: no shadow, no radius, no lift, no scale. Its hover state is
   the .ledger--titles row treatment — --surface fill plus a --teal
   inline-start rule — because it replaces a register row and should read as
   one. The card shape is here because the thing is a tap target for a
   drawer, and a table row is a poor tap target on a phone.
   ========================================================================== */

/* --- 13.1 .rail — the category filter ----------------------------------- */
/* A filter, never a configurator. It narrows .sitegrid and touches nothing
   else on the page.

   Sticky under the masthead, like §11.2's .pickbar was — and for the same
   reason §12.1 ends the band entrance on `transform: none` rather than
   translateY(0): a surviving transform would make the band a containing
   block and this rail would stop following the masthead for the rest of the
   session. That is a real bug that shipped once; the token contract is what
   prevents it, so do not "tidy" §12.1's end state back to translateY(0). */

.rail {
  position: sticky;
  inset-block-start: var(--masthead-h);
  z-index: 1;
  margin-block: var(--sp-1);
  padding-block: var(--sp-h);
  background-color: var(--ground);
  border-block-end: var(--rule-section) solid var(--ink);
}

.rail__list {
  display: flex;
  gap: var(--sp-h);
  list-style: none;
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scroll-snap-type: x proximity;
  /* The scrollbar is a scrollbar; it does not need styling to be understood.
     padding-block-end holds it clear of the pills on the platforms that
     reserve space for one. */
  padding-block-end: var(--sp-q);
}

/* The five umbrella names are long — "Consumer rights & accountability" alone
   sets ~277px — and all five plus "All" need ~1498px against the ~1046px the
   wrap gives them at 1440. Left as a scroller, two of the five filters sat
   off-screen behind a scrollbar on a desktop that had room to show them.
   The carousel is a PHONE affordance: it stays below 48rem, where swiping a
   rail is the expected gesture, and above that the pills simply wrap. */
@media (min-width: 48rem) {
  .rail__list {
    flex-wrap: wrap;
    overflow-x: visible;
    scroll-snap-type: none;
    padding-block-end: 0;
  }
}

/* `position: relative` is load-bearing, not tidiness. base.css's
   .visually-hidden is `position: absolute` with no offsets, so its containing
   block is the nearest POSITIONED ancestor — which, without this line, is
   `.rail` (sticky counts as positioned). The six hidden radios then resolved
   against the rail instead of their own <li>, escaped the scroll container,
   and took their static positions out to 1272px: a 375px phone got a page
   that scrolled sideways to 1298px. Containing each input in its own item
   keeps the overflow where it belongs, inside .rail__list. */
.rail__item {
  position: relative;
  flex: 0 0 auto;
  scroll-snap-align: start;
}

/* The input is the state. The pill is a <label> wrapping a visually-hidden
   <input type=radio>, so the whole filter is keyboard-operable and
   arrow-key-navigable as a radio group with no JS at all — the same device
   .chip uses for the site picker in §10. */
.rail__pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-q);
  min-block-size: var(--tap);
  padding-inline: var(--sp-1);
  border: var(--rule-section) solid var(--rule);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  cursor: pointer;
  white-space: nowrap;
}

.rail__pill:hover {
  background-color: var(--surface);
  border-color: var(--teal-deep);
}

.rail__input:focus-visible + .rail__pill {
  outline: var(--rule-section) solid var(--focus-c);
  outline-offset: var(--rule-section);
}

.rail__input:checked + .rail__pill {
  background-color: var(--ink);
  border-color: var(--ink);
  color: var(--on-ink);
}

/* The per-category count. Tabular so the pills do not re-flow as the
   numbers change width. */
.rail__n {
  font-variant-numeric: tabular-nums;
  opacity: 0.7;
}

@media (prefers-reduced-motion: no-preference) {
  .rail__pill {
    transition:
      background-color var(--dur) var(--ease),
      border-color var(--dur) var(--ease),
      color var(--dur) var(--ease);
  }
}

/* --- 13.2 .sitegrid / .sitecard — the page's centre of gravity ----------- */

/* minmax(0, 1fr), never a bare 1fr. A grid item's default `min-width: auto`
   floors the track at the item's min-content width, and the longest umbrella
   tag ("Consumer rights & accountability", tracked caps) is wider than a
   375px phone column — so a bare 1fr pushed every card to 371px inside a
   323px space and scrolled the whole page sideways. The explicit 0 minimum
   lets the track shrink and the text wrap instead. */
.sitegrid {
  display: grid;
  gap: var(--sp-1);
  grid-template-columns: minmax(0, 1fr);
  list-style: none;
  margin-block-start: var(--sp-1h);
}

.sitegrid__item {
  min-inline-size: 0;
}

@media (min-width: 48rem) {
  .sitegrid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 64rem) {
  .sitegrid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* Hidden by the filter. `hidden` alone would be overridden by the grid
   display, so the attribute selector restates it. */
.sitegrid__item[hidden] {
  display: none;
}

.sitecard {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--sp-q);
  inline-size: 100%;
  min-inline-size: 0;
  block-size: 100%;
  padding: var(--sp-1);
  text-align: start;
  background-color: var(--ground);
  border: var(--rule-section) solid var(--rule);
  border-inline-start: var(--rule-sig) solid var(--rule);
  cursor: pointer;
  font: inherit;
  color: inherit;
}

.sitecard:hover {
  background-color: var(--surface);
  border-color: var(--teal-deep);
  border-inline-start-color: var(--teal);
}

.sitecard__accession {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-caps);
  font-variant-numeric: tabular-nums;
  color: var(--fg-mute);
}

.sitecard__name {
  font-family: var(--font-display);
  font-size: var(--fs-sub);
  line-height: var(--leading-sub);
}

.sitecard__desc {
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
  /* Descriptions in portfolio.json run to three sentences; the card shows the
     opening and the drawer shows all of it. Clamping is a display decision,
     not a truncation of the record — the full text is in the DOM. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

.sitecard__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-h);
  inline-size: 100%;
  margin-block-start: auto;
  padding-block-start: var(--sp-h);
}

.sitecard__from {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-lede);
  font-variant-numeric: tabular-nums;
  color: var(--fg-accent);
}

.sitecard__from-unit {
  font-weight: var(--weight-body);
  font-size: var(--fs-micro);
  color: var(--fg-mute);
}

@media (prefers-reduced-motion: no-preference) {
  .sitecard {
    transition:
      background-color var(--dur) var(--ease),
      border-color var(--dur) var(--ease);
  }
}

/* --- 13.3 .drawer — the configurator shell ------------------------------ */
/* Right-edge above 48rem, full-screen sheet below it. Focus trap, Esc close
   and focus return are all <dialog>.showModal() behaviour — this project
   does not reimplement any of the three.

   Same single-scroll-column decision as §10's .cart, and for the same
   measured reason: as a flex sandwich the body collapsed to zero height once
   the order-details fields appeared, and the buyer could not see what they
   were ordering. Head and foot are sticky; the middle scrolls. */

.drawer {
  position: fixed;
  inset-block: 0;
  inset-inline-end: 0;
  inset-inline-start: auto;
  z-index: var(--z-drawer);
  inline-size: min(var(--config-w), 100%);
  max-inline-size: 100%;
  block-size: 100%;
  max-block-size: 100%;
  margin: 0;
  padding: 0;
  display: block;
  overflow-y: auto;
  overscroll-behavior: contain;
  --ground: var(--white);
  --fg: var(--ink);
  --fg-mute: var(--teal-deep);
  --rule: var(--line);
  --rule-strong: var(--teal-deep);
  --focus-c: var(--teal);
  background-color: var(--ground);
  color: var(--fg);
  border: 0;
  border-inline-start: var(--rule-section) solid var(--ink);
}

.drawer:not([open]) {
  display: none;
}

.drawer::backdrop {
  background-color: var(--scrim);
}

/* The persistent header: name, running total, item count. The total is
   visible at EVERY step — the whole point of the header — so the checkout
   figure is never the first time a number appears. */
.drawer__head {
  position: sticky;
  inset-block-start: 0;
  z-index: 1;
  padding: var(--sp-1) var(--sp-1h);
  --ground: var(--ink);
  --fg: var(--on-ink);
  --fg-mute: var(--on-ink);
  --rule: var(--rule-on-ink);
  --rule-strong: var(--rule-on-ink-strong);
  --focus-c: var(--cyan);
  background-color: var(--ground);
  color: var(--fg);
}

.drawer__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-1);
}

.drawer__title {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-fine);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
  min-inline-size: 0;
}

.drawer__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--tap);
  min-block-size: var(--tap);
  flex: 0 0 auto;
  color: var(--on-ink);
}

.drawer__running {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-1);
  margin-block-start: var(--sp-h);
  padding-block-start: var(--sp-h);
  border-block-start: var(--hair) solid var(--rule);
  font-size: var(--fs-fine);
  font-variant-numeric: tabular-nums;
}

/* The one cyan occurrence on this page's drawer, on --ink at 6.53:1.
   Budget: 1. See CLAUDE.md CYAN RULE. */
.drawer__running-total {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-sub);
  color: var(--cyan);
}

.drawer__body {
  padding: var(--sp-1h);
}

/* Empty cart is a .motif instance and a line of copy — never a blank panel.
   docs/design-system.md §5.9. */
.drawer__body[data-state="empty"] {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding-block: var(--sp-2);
  color: var(--fg-mute);
  font-size: var(--fs-fine);
}

.drawer__foot {
  position: sticky;
  inset-block-end: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-h);
  padding: var(--sp-1) var(--sp-1h);
  background-color: var(--surface);
  border-block-start: var(--rule-section) solid var(--ink);
}

.drawer__foot .btn {
  flex: 1 1 auto;
}

.drawer__back {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-q);
  min-block-size: var(--tap);
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--fg-mute);
}

.drawer__back::before {
  content: "\2190";
  /* A character, not an icon. Same rule as .link--out's trailing glyph. */
}

/* --- 13.4 .stepdots — the step indicator -------------------------------- */
/* Three dots, not a progress bar: the flow is three steps long and a bar
   would overstate it. The active step is carried in TEXT for a screen
   reader (.stepdots__label), never by fill alone. */

.stepdots {
  display: flex;
  align-items: center;
  gap: var(--sp-h);
  margin-block-end: var(--sp-1);
  list-style: none;
}

.stepdots__dot {
  inline-size: var(--sp-h);
  block-size: var(--sp-h);
  flex: 0 0 auto;
  background-color: var(--rule);
}

.stepdots__dot[aria-current="step"] {
  background-color: var(--teal);
}

.stepdots__dot[data-done] {
  background-color: var(--teal-deep);
}

.stepdots__label {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--fg-mute);
  margin-inline-start: var(--sp-q);
}

/* --- 13.5 .svc — step 1's service tiles --------------------------------- */
/* Up to four large tap targets, one per core service, filtered to what the
   scoped title or package actually offers. This is the high-level choice,
   NOT the catalogue — the catalogue is what v1 got wrong. */

.svc {
  display: grid;
  gap: var(--sp-h);
  list-style: none;
}

.svc__btn {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--sp-q) var(--sp-1);
  inline-size: 100%;
  min-block-size: var(--tap);
  padding: var(--sp-1);
  text-align: start;
  background-color: var(--ground);
  border: var(--rule-section) solid var(--rule);
  border-inline-start: var(--rule-sig) solid var(--rule);
  cursor: pointer;
  font: inherit;
  color: inherit;
}

.svc__btn:hover {
  background-color: var(--surface);
  border-color: var(--teal-deep);
  border-inline-start-color: var(--teal);
}

.svc__name {
  font-family: var(--font-display);
  font-size: var(--fs-lede);
  line-height: var(--leading-lede);
}

.svc__price {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
  color: var(--fg-accent);
  text-align: end;
}

.svc__plain {
  grid-column: 1 / -1;
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
}

@media (prefers-reduced-motion: no-preference) {
  .svc__btn {
    transition:
      background-color var(--dur) var(--ease),
      border-color var(--dur) var(--ease);
  }
}

/* --- 13.6 .fieldgroup — step 2, one group visible at a time ------------- */
/* "One field group visible at a time, not a wall of inputs" is the binding
   requirement here. The groups are <fieldset>s and the inactive ones are
   `hidden`, so the wall never assembles. */

.fieldgroup {
  border: 0;
  padding: 0;
  margin: 0;
}

.fieldgroup__legend {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--fg-mute);
  margin-block-end: var(--sp-h);
  padding: 0;
}

.fieldgroup + .fieldgroup {
  margin-block-start: var(--sp-1h);
  padding-block-start: var(--sp-1h);
  border-block-start: var(--hair) solid var(--rule);
}

/* Upgrade toggles. Chips with their own price delta shown ON the chip — an
   upgrade whose cost is one screen away is an upgrade that gets disputed on
   the invoice. */
.toggles {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-h);
  list-style: none;
}

/* Same containing-block reason as .rail__item above: the hidden checkbox must
   resolve against its own item, not against the fixed-position drawer. */
.toggles > li {
  position: relative;
}

.toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-q);
  min-block-size: var(--tap);
  padding-inline: var(--sp-1);
  border: var(--rule-section) solid var(--rule);
  font-size: var(--fs-fine);
  cursor: pointer;
}

.toggle:hover {
  background-color: var(--surface);
  border-color: var(--teal-deep);
}

.toggle__input:focus-visible + .toggle {
  outline: var(--rule-section) solid var(--focus-c);
  outline-offset: var(--rule-section);
}

.toggle__input:checked + .toggle {
  background-color: var(--surface);
  border-color: var(--teal);
  border-inline-start-width: var(--rule-sig);
}

.toggle__delta {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
  color: var(--fg-accent);
}

/* --- 13.7 .line — the cart-state line item ------------------------------ */

.linelist {
  list-style: none;
}

.line {
  padding-block: var(--sp-1);
  border-block-end: var(--hair) solid var(--rule);
}

.line__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-h);
}

.line__name {
  font-family: var(--font-display);
  font-size: var(--fs-lede);
  line-height: var(--leading-lede);
}

.line__total {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}

.line__meta {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--sp-q) var(--sp-h);
  margin-block-start: var(--sp-h);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
}

.line__meta dt {
  font-family: var(--font-display);
  font-weight: var(--weight-caps);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--fg-mute);
}

.line__meta dd {
  word-break: break-word;
}

.line__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-h);
  margin-block-start: var(--sp-h);
}

/* --- 13.8 .done — the confirmation state -------------------------------- */

.done__ref {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-head);
  line-height: var(--leading-head);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-caps);
  margin-inline-end: calc(var(--track-caps) * -1);
  margin-block: var(--sp-h);
}

.done__note {
  max-inline-size: var(--measure);
  font-size: var(--fs-fine);
  line-height: var(--leading-fine);
  color: var(--fg-mute);
}

/* --- 13.9 .totals — the step-4 arithmetic ------------------------------- */

.totals {
  margin-block-start: var(--sp-1);
  padding-block-start: var(--sp-1);
  border-block-start: var(--rule-section) solid var(--ink);
}

.totals__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-1);
  padding-block: var(--sp-q);
  font-size: var(--fs-fine);
  font-variant-numeric: tabular-nums;
}

.totals__row--grand {
  margin-block-start: var(--sp-h);
  padding-block-start: var(--sp-h);
  border-block-start: var(--hair) solid var(--rule);
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--fs-sub);
}

/* --- 13.10 .packrail — the four non-featured packages -------------------- */
/* NETWORK 15 is not in here: it owns the whole of ink band ② on its own,
   per docs/design-system.md §5.5. These four sit under it as ordinary rows,
   and the dominance comes from that difference in treatment rather than from
   a ribbon on one card. Inside an --ink band, so .svc__btn's ground tokens
   are the band's, not white. */

.packrail {
  display: grid;
  gap: var(--sp-h);
  grid-template-columns: 1fr;
  list-style: none;
  margin-block-start: var(--sp-1);
}

@media (min-width: 48rem) {
  .packrail {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* The one line that has to be said out loud: inside .band--ink the shared
   .svc__btn skin would paint a white card on a dark ground. It reads the
   band's ground tokens instead, so the tile is a bordered transparent block
   with frost text — the .btn--ink treatment, applied to a tile. */
.band--ink .svc__btn {
  background-color: transparent;
  border-color: var(--rule-on-ink);
  border-inline-start-color: var(--rule-on-ink-strong);
  color: var(--on-ink);
}

.band--ink .svc__btn:hover {
  background-color: var(--teal-deep);
  border-color: var(--on-ink);
  border-inline-start-color: var(--cyan);
}

.band--ink .svc__price,
.band--ink .svc__plain {
  color: var(--on-ink);
}

/* --- 13.11 .drawer__back-line — the back control's own line ------------- */

.drawer__back-line {
  margin-block-end: var(--sp-1);
}
