/* ShelfPrice - site styles. One sheet, three pages.
 *
 * ⛔ THIS IS A SALES PAGE. IT NEVER REPORTS THE PROJECT'S STATE TO A CUSTOMER.
 * Standing rule, Andrew 2026-08-23, full text in agents/build/skills.md: no release status, no
 * "not on the stores", no "come back when it ships", no counters of nothing, no roadmap as copy.
 * The status board and its `.counters` rules are deleted below rather than left orphaned, so
 * nothing can quietly reach for them again, and instrument.js no longer emits the figures at all
 * (a page asking for them now FAILS THE BUILD). Project state belongs in STATE.md.
 *
 * THE VISUAL SYSTEM. Rewritten 2026-08-23; this describes the CODE, not an intention. Andrew: "it
 * still looks like an ai slop site ... make it more professional and clean like StudyRoster is",
 * then "be sure you're not just copying studyroster exactly".
 *
 * ONE GROUND, NOT TWO. The page is near-white paper. The field below is a whisper on it (base
 * chroma 8-10, bloom alpha .12-.16). The previous field was chroma 40 and read as wallpaper, and
 * four consecutive rejections were all treated as HUE complaints when the wrong variable was
 * INTENSITY. Measured against StudyRoster, the site Andrew has approved: its ground is chroma 4-8
 * with 86% of painted pixels at chroma <= 20 -- a heavier construction than ours, spent at a tenth
 * of the strength.
 *
 * COLOUR IS SPENT IN TWO PLACES AND NOWHERE ELSE: the accent, and the closing band.
 *   THE ACCENT MEANS "THIS IS YOU". Your position in a comparison, and nothing else. Not a brand
 *   tint, not a link colour, not a button fill, never a status. It is now the same hue family as
 *   the field; it used to be violet on a blue field, 70 degrees apart, so the page's only two
 *   colours belonged to different brands.
 *   THE CLOSING BAND is the one saturated surface, near-black, where the page asks for the install.
 *   The mid-page dark panel is GONE: it was the only dark surface on a light page, carried a lilac
 *   bar from a palette this site no longer uses, and read as a foreign object dropped in.
 *
 * -- RULES AND ALIGNMENT, NOT CARDS AND SHADOWS -------------------------------------------------
 * Structure comes from hairlines, a spacing scale and tabular alignment. No drop shadow on any data
 * object, and three radii in total.
 * NO SINGLE PRIMITIVE DOES EVERY JOB. The previous build had SIX instances of "label left, value
 * right, hairline under" on one page: four ledgers, the stat block and the figure rows. That is the
 * house-template failure one layer down, and monotony is most of what reads as generated. Each now
 * has its own shape: `.narrow` is a reduction, `.verdict` a payoff line, `.trap` a framed callout,
 * `.rules` readable statements, `.cite-block` a citation. `.ledger` survives on the prose pages.
 *
 * -- ONE TYPEFACE -------------------------------------------------------------------------------
 * The old build set 98 of 277 leaf elements in monospace, 45 uppercase and 44 letter-spaced: every
 * label, axis, caption, table cell and badge. A marketing page rendered as a terminal dump reads as
 * a spec sheet, and it was the loudest slop signal on the site. Numbers now use the sans face with
 * tabular figures, which is what real financial products do and which aligns just as well.
 * Monospace survives for ONE job: literal code identifiers on the privacy page.
 * Uppercase survives in TWO roles: the eyebrow, and the shop table's column header.
 *
 * -- WHY THE TYPE IS RESTRAINED -----------------------------------------------------------------
 * Measured off the live peer sites, not guessed. Largest type on the page:
 *   Blacklight 22px | Privacy Badger 32px | ClearURLs 32px | uBlock Origin 40px | SponsorBlock 29px
 * An earlier version ran an 86px headline alone on a 1050px screen. Display caps at 52px. Body is
 * 16px on a 1.6 line, up from 13.5: the approved reference and the peer set both land at 15-16. The
 * size scale is a real 1.2 modular ratio; the old one was eight arbitrary sizes with a hole in it.
 * See PEER-STUDY.md for the measurements.
 *
 * THERE IS JAVASCRIPT ON THIS SITE. boot.js and site.js, both same-origin. They make no network
 * call, set no cookie and touch no storage, which is the claim the privacy page makes and the only
 * one that matters. A comment here once asserted the reverse of the code; on a site whose thesis is
 * "do not accept a claim you cannot check", view-source is the first thing a sceptic opens.
 *
 * NO CONTAINER QUERIES. Firefox does not honour a container query made from a pseudo-element of the
 * query container itself, and that silently deleted this site's signature mechanism in the browser
 * it is reviewed in while every Chrome proof stayed green.
 *
 * NO SCROLL-DRIVEN CSS. `animation-timeline: view()/scroll()` is not available in the review
 * browser, and a scroll effect that renders at its static floor is how a page gets called basic.
 * Scroll work is site.js: one rAF loop, one read pass then one write pass, IntersectionObserver
 * gated, and it runs in every engine.
 *
 * NO PERSPECTIVE. A previous build gave the tag field `perspective:1000px`; projected children
 * landed outside the rail, projected overflow counts toward scrollable overflow, and the document
 * carried a horizontal scrollbar at every width from 768 to 1400 for four consecutive versions
 * while both tested widths stayed clean. The depth here is 2D travel, which cannot overflow a box
 * it is not already in.
 *
 * NO WEBFONTS. There is no `font-src` in the CSP, so neither a CDN font nor a self-hosted one can
 * load. The type system is the system sans stack used properly.
 */

/* ==GRADIENT-START== */
/* brandHue 200, arcSpan 12%, satEven 90%.
   GENERATED by build-palette.js from kit/gradient/gradient-core.js - do not hand-edit.
   This block is EXACTLY exportCSSFromToken(token below). No post-transform, so it reproduces.
   Measured base stops: hue 195 -> 202 -> 210 -> 216 (anchors of 13 stops) (travel 29 deg), S 36-45, L 96-96.
   ARC (arcSpan) is a CRAFT setting, fixed by Art on the render: at 100% this page ran mint to hot
      magenta and read as two brands top to bottom.
   EVEN MIDDLE (satEven) is also CRAFT, fixed by Art on the render: at 0% the field lost 25.5
      points of saturation through the middle of the page and read as near-white with a cyan
      cast. 90% is the knee - the base anchor spread bottoms out there and every higher setting
      only drains the blooms' chroma variety. The ~9 points left are what a multi-hue field IS.
   HUE (brandHue): Andrew AUTHORISED a change on 2026-08-23 ("touch up the color palette because
      right now it's rather offputting"), which reversed the standing out-of-scope hold. Rejected
      on this site already: hot magenta twice, mint once (it fought the violet brand type), and
      the violet-to-PINK field this replaced. The arc travels UP in hue, so the lever that kills a
      pink foot is starting lower, not tweaking the end. All three are one command:
      bash /home/andrew/Project/work/units/web-agency/kit/
      gradient/make-gradient-site.sh shelfprice-site */
body {
  /* generated by kit/gradient — token: {"s1":0,"s2":34,"s3":-72,"brandHue":200,"arcSpan":12,"satEven":90} */
  background:
  radial-gradient(1300px 1196px at 18% 4%, rgba(173,214,231,0.134) 0%, transparent 74%),
  radial-gradient(1200px 1104px at 76% 8%, rgba(179,212,232,0.126) 0%, transparent 74%),
  radial-gradient(1150px 1058px at 44% 13%, rgba(174,215,231,0.122) 0%, transparent 74%),
  radial-gradient(1250px 1150px at 90% 18%, rgba(180,212,232,0.130) 0%, transparent 74%),
  radial-gradient(1200px 1104px at 9% 24%, rgba(167,210,229,0.126) 0%, transparent 74%),
  radial-gradient(1350px 1242px at 62% 29%, rgba(187,214,234,0.122) 0%, transparent 74%),
  radial-gradient(1200px 1104px at 33% 35%, rgba(180,214,232,0.126) 0%, transparent 74%),
  radial-gradient(1250px 1150px at 84% 41%, rgba(177,211,231,0.126) 0%, transparent 74%),
  radial-gradient(1300px 1196px at 14% 47%, rgba(181,212,232,0.122) 0%, transparent 74%),
  radial-gradient(1250px 1150px at 54% 53%, rgba(187,214,234,0.126) 0%, transparent 74%),
  radial-gradient(1200px 1104px at 80% 59%, rgba(185,213,233,0.126) 0%, transparent 74%),
  radial-gradient(1300px 1196px at 30% 65%, rgba(176,199,231,0.122) 0%, transparent 74%),
  radial-gradient(760px 699px at 68% 71%, rgba(180,203,232,0.157) 0%, transparent 74%),
  radial-gradient(1200px 1104px at 16% 78%, rgba(169,193,229,0.122) 0%, transparent 74%),
  radial-gradient(720px 662px at 58% 85%, rgba(171,194,230,0.157) 0%, transparent 74%),
  radial-gradient(1250px 1150px at 38% 93%, rgba(150,179,226,0.130) 0%, transparent 74%),
  linear-gradient(165deg, rgb(240,246,248) 0%, rgb(240,246,248) 9.5%, rgb(240,245,248) 19%, rgb(240,245,248) 28.5%, rgb(240,245,248) 38%, rgb(240,245,248) 46.5%, rgb(240,244,248) 55%, rgb(240,244,248) 63.5%, rgb(240,244,248) 72%, rgb(240,244,248) 79%, rgb(240,243,249) 86%, rgb(240,243,249) 93%, rgb(239,243,249) 100%);
  background-attachment: scroll;
}
/* ==GRADIENT-END== */

/* OUTSIDE the generated region ON PURPOSE, so the block above stays byte-identical to
   exportCSSFromToken(its token) and can be checked against it.
   The generator sets `background:` and `background-attachment` and nothing else, which leaves
   `background-repeat` at its default of `repeat`. On a short page that is invisible; on a very long
   page the 16-bloom stack TILES and a hard seam appears where the field starts over. */
body{background-repeat:no-repeat; min-height:100vh}

/* ═══ THE DESIGN SYSTEM ═════════════════════════════════════════════════════════════════════════
 * Rewritten 2026-08-23 against a MEASURED reference. Andrew: *"it still looks like an ai slop site.
 * review the text, the spacing of the page, the color gradient, the style features. make it more
 * professional and clean like StudyRoster is."* Followed by: *"be sure you're not just copying
 * studyroster exactly ... I don't want a clone of studyroster with a different function."*
 *
 * SO WHAT WAS TAKEN IS THE DISCIPLINE, NOT THE DESIGN. StudyRoster's build-next tree was rendered
 * and its computed styles dumped; the numbers that transferred are the RATIOS and the RESTRAINT:
 *   - one typeface, no mono                     (ShelfPrice had 98 of 277 leaf elements in mono)
 *   - a page that is 86% white or chroma <= 20  (ShelfPrice's ground was chroma 40, i.e. wallpaper)
 *   - colour spent in two deliberate set-pieces, not spread across the page
 *   - headings margin:0, the following element owns the gap
 *   - line-height bimodal: ~1.1 headings, ~1.6 body
 *   - section padding that does NOT collapse on mobile
 *   - 11 of 14 effects hover-only, zero keyframes, no parallax, no pinned tracks
 *   - a three-tier radius system
 *
 * WHAT WAS DELIBERATELY NOT TAKEN: its look. StudyRoster is CARD-BASED AND SOFT — white cards
 * floating on white with soft shadows, 999px pills everywhere, centred section heads, indigo/violet
 * brand gradient, and rendered product mocks doing the visual work. A tutoring SaaS sells calm.
 * SHELFPRICE IS RULE-BASED AND PRECISE: hairlines and alignment instead of cards and shadows,
 * left-aligned data-forward heads, tabular figures, three radii used sparingly, one blue that means
 * exactly one thing, and DATA INSTRUMENTS rather than UI screenshots. A price comparison tool sells
 * a number and a gap. Same standard, opposite expression: a stranger seeing both should not guess
 * they came from one template.                                                                     */

:root{
  /* ── Ink ────────────────────────────────────────────────────────────────────────────────────
     RE-MEASURED FOR THE NEW FIELD. --sub and --faint were #353b42 / #383f46, darkened in the
     previous pass to survive a chroma-40 ground that reached L 86%. The ground is now L 97%, so
     the same legibility buys much better typographic colour: 7.0:1 and 5.5:1 against the field's
     nominal ground, re-verified against the MEASURED darkest painted point by field-probe.cjs +
     check-deploy.cjs check 9. Text that is darker than it needs to be is its own defect — every
     label on the old page sat at near-black and the page read as uniformly heavy. */
  --ink:#14181a;
  --sub:#4b5563;
  /* #5b636d measured 4.05:1 and FAILED. The nominal ground is rgb(240,246,248), but the field's
     DARKEST painted point is #cfd3da — a colour picked against the ground is picked against a page
     that does not exist. #525963 is 4.71:1 on the measured point. This is the same trap the previous
     pass documented and it caught this pass too, which is the check earning its keep. */
  --faint:#525963;
  --line:rgba(20,24,26,.13);
  --line-soft:rgba(20,24,26,.07);
  --paper:#fff;

  /* ── The one accent ─────────────────────────────────────────────────────────────────────────
     ⛔ IT MEANS "THIS IS YOU". Not a brand tint, not a link colour, not a button fill, never a
     status. It marks the reader's own position in a comparison and nothing else, which is the
     product's entire idea.
     CHANGED from #6b3f9e (violet) 2026-08-23. The field is hue ~200 (blue) and the accent sat at
     hue ~272: 70 degrees apart, so the page's only two colours belonged to different brands and
     neither read as chosen. It is now the same family as the field, which means the whole site
     carries exactly ONE hue — maximal restraint, and the reason a single blue tick among black
     ones is instantly findable. 5.9:1 on the field, 6.9:1 on white. */
  --lit:#0b57d0;
  --lit-soft:rgba(11,87,208,.09);
  --lit-line:rgba(11,87,208,.32);

  /* The two dark set-pieces, and there are exactly two: the closing band and the prose pull-quote. */
  --dark:#14181d;
  --dark-ink:#f4f6f8;
  --dark-sub:#a7b0bb;
  --dark-line:rgba(255,255,255,.14);

  /* ── Type scale. A real one: 1.2 modular, rounded, with a display jump at the top. ───────────
     The old page had EIGHT sizes — 10.5, 11, 13.5, 14.5, 15, 31, 52, 58 — with ratios of 1.68,
     2.07, 1.11 and 1.23 between neighbours and a hole between 15 and 31. That is not a scale, it
     is eight separate decisions, and it is a large part of why the page read as assembled rather
     than designed. Body moves 13.5 -> 16: the measured reference sets body at 16/1.6 and the
     price-comparison peer set lands 14-16. Nothing on this page is smaller than 12. */
  --t-12:12px;   /* eyebrow, table header, the only two uppercase roles on the site */
  --t-14:14px;   /* captions, secondary values, footnotes */
  --t-16:16px;   /* body */
  --t-19:19px;   /* lede, claim line */
  --t-24:24px;   /* h3 */
  --t-32:clamp(24px,2.9vw,32px);   /* h2 */
  --t-52:clamp(32px,4.4vw,52px);   /* h1 */
  --t-num:clamp(34px,4vw,44px);    /* the one display figure */

  /* Four weights, down from eight (400/500/600/660/670/750/780/800). */
  --w-reg:400; --w-med:500; --w-semi:600; --w-bold:700;

  /* ── Spacing scale. 4px base. Every gap on the page is one of these. ────────────────────────
     The old page used a flat 46px section padding for all six sections, so no section carried
     more weight than any other, and the internal gaps were authored per component. */
  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px; --s7:48px; --s8:64px;
  /* Section rhythm. The reference holds 84px at BOTH widths and only lets the hero compress; a
     page that halves its air on mobile stops reading as the same document. This compresses a
     little, not by a third. */
  --sec:clamp(60px,6.4vw,88px);

  /* ── Radii: three tiers, and that is the whole set. ─────────────────────────────────────────
     The old stylesheet used NINE (1,2,4,5,6,8,10,12,14). Nine radii is nobody's decision. */
  --r-sm:4px;    /* badges, chips, the tick marker */
  --r-md:8px;    /* buttons, focus rings */
  --r-lg:14px;   /* panels, the readout, the closing band */

  --maxw:1180px;
  --gut:clamp(20px,5vw,56px);
  --rail:158px;

  /* ⛔ ONE TYPEFACE. --mono survives for exactly one job: literal code identifiers on the privacy
     page (`is_marketplace_third_party`), where a monospace face is genuinely EARNING its place by
     making a field name legible as a field name. It is used nowhere else on the site.
     WHY THIS IS THE BIGGEST SINGLE CHANGE: the previous build set 98 of 277 leaf elements in
     monospace, 45 in uppercase and 44 with positive letter-spacing — every label, every axis, every
     caption, every table cell and every badge. A marketing page rendered as a terminal dump reads
     as a spec sheet, and it was the loudest "slop" signal on the page. Numbers now use the sans
     face with tabular figures, which is what actual financial products do and which aligns just as
     well as mono while looking designed rather than dumped. */
  --sans:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0; font-family:var(--sans); font-size:var(--t-16); line-height:1.6;
  color:var(--ink); font-weight:var(--w-reg);
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
}
/* Tabular figures wherever a number can sit in a column. Set once, at the root of the numeric
   components, rather than re-declared on eleven selectors. */
.num,.sp-total,.cstats .v,.strip-axis .v,.cmp-ledger .v,.sr-item,.sr-ship,.sr-total,.sr-n,
.mfig-rows .v,.mfig-rows .p,.narrow .n{font-variant-numeric:tabular-nums}

img,svg{max-width:100%}
p{margin:0 0 1em}
p:last-child{margin-bottom:0}
strong{font-weight:var(--w-semi); color:var(--ink)}
a{color:inherit}

/* ── Type roles ─────────────────────────────────────────────────────────────────────────────────
   Headings are margin:0 and the FOLLOWING element owns the gap. Taken from the reference, and it is
   the thing that makes vertical rhythm controllable: a heading never carries space it might not
   need, so there is exactly one place to change the distance between a head and its content. */
.h1{font-size:var(--t-52); line-height:1.06; letter-spacing:-.026em; font-weight:var(--w-bold); margin:0}
.h2{font-size:var(--t-32); line-height:1.14; letter-spacing:-.021em; font-weight:var(--w-bold); margin:0}
.h3{font-size:var(--t-24); line-height:1.2;  letter-spacing:-.016em; font-weight:var(--w-bold); margin:0}
.lede{font-size:var(--t-19); line-height:1.6; color:var(--sub); font-weight:var(--w-reg)}

/* ⛔ THE ONLY UPPERCASE ROLE ON THE SITE, besides the shop table's column header.
   Uppercase + letter-spacing is a real typographic device and it works precisely because it is
   RATIONED — the reference uses it in two roles and nowhere else. The previous build used it in
   forty-five places, at which point it stops being emphasis and becomes texture. */
.tag{
  font-size:var(--t-12); letter-spacing:.085em; text-transform:uppercase;
  color:var(--faint); font-weight:var(--w-bold); line-height:1.5;
}
.measure{max-width:36em}
.measure-tight{max-width:28em}
.note{font-size:var(--t-14); color:var(--faint); line-height:1.6; max-width:38em}

/* ── Shell ─────────────────────────────────────────────────────────────────────────────────────── */
.wrap{max-width:var(--maxw); margin:0 auto; padding-inline:var(--gut)}
.skip{
  position:absolute; left:-9999px; top:0; background:var(--ink); color:#fff;
  padding:var(--s3) var(--s4); z-index:50; border-radius:0 0 var(--r-md) 0; font-weight:var(--w-semi);
}
.skip:focus{left:0}

header.site{padding:var(--s5) 0 0}
.bar{display:flex; align-items:center; justify-content:space-between; gap:var(--s5); flex-wrap:wrap}
.brand{
  display:inline-flex; align-items:center; gap:10px; text-decoration:none;
  font-weight:var(--w-bold); letter-spacing:-.022em; font-size:18px;
}
.brand svg{display:block; width:24px; height:24px; border-radius:var(--r-sm)}
nav.site ul{display:flex; gap:var(--s5); list-style:none; margin:0; padding:0; flex-wrap:wrap}
nav.site a{
  text-decoration:none; font-weight:var(--w-med); font-size:15px; color:var(--sub);
  padding:var(--s1) 0; display:inline-block; border-bottom:2px solid transparent;
}
nav.site a:hover{color:var(--ink); border-bottom-color:var(--ink)}
nav.site a[aria-current="page"]{color:var(--ink); border-bottom-color:var(--ink)}

/* ── Layout primitives ─────────────────────────────────────────────────────────────────────────── */
.rail{display:grid; grid-template-columns:var(--rail) minmax(0,1fr); gap:clamp(20px,3vw,44px); align-items:start}
.rail>.tag{padding-top:5px}
.split{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:clamp(26px,4vw,56px); align-items:start}
.stack>*+*{margin-top:var(--s5)}
section{padding-block:var(--sec)}
.band{padding-block:var(--sec)}

/* ── Hero ──────────────────────────────────────────────────────────────────────────────────────
   padding-BLOCK, never the shorthand: `.hero` is also a `.wrap` and the shorthand resets the
   inline gutter, which put the headline flush against the window edge at every width below the
   max-width and was invisible at 1500 because the centring hid it. */
.hero{padding-block:clamp(40px,4.6vw,64px) clamp(24px,3vw,40px)}
.hero-grid{
  display:grid; grid-template-columns:minmax(0,1.08fr) minmax(0,.92fr);
  gap:clamp(32px,4.5vw,64px); align-items:center;
}
.hero .tag{margin-bottom:var(--s4); display:block}
.hero .h1{max-width:15ch}
.hero .lede{margin-top:var(--s5); max-width:32em}

/* ── The creed. THE CAMPAIGN'S VOICE, and the only component allowed to have one. ──────────────
   Added 2026-08-23 with the movement reframe. Andrew: *"we need to market it as a 'grass roots,
   fighting back against big corporations getting away with near-theft and price gouging' type of
   movement"*.

   ⛔ IT IS A TYPOGRAPHIC TURN, NOT A PULL QUOTE. Two lines: the world everyone already understands,
   set quiet and regular, then what replaced it, set in full ink and semibold. The whole argument of
   the site is the distance between those two lines, so the component's only job is to make that
   distance visible. There is no border, no card and no accent: this page spends colour in exactly
   two places and neither of them is here, and a quote mark or a left rule would make an assertion
   look like a decoration.

   ⛔ IT APPEARS EXACTLY TWICE and both are places the page SPEAKS rather than MEASURES: under the
   headline, and over the regulator's finding. That restraint is the point. Six instances of one
   primitive is the defect this build was rewritten to remove, and a voice used everywhere is not a
   voice, it is a texture. Every other section on this page states a number and shuts up. */
.creed{
  list-style:none; margin:var(--s5) 0 0; padding:0; max-width:26em;
  font-size:var(--t-24); line-height:1.34; letter-spacing:-.017em;
}
.creed li{margin:0}
.creed .was{color:var(--faint); font-weight:var(--w-reg)}
.creed .now{color:var(--ink); font-weight:var(--w-semi); margin-top:var(--s2)}
/* In the charge section it follows an h2 and precedes the citation, so it carries its own air. */
#why .creed{margin-top:var(--s5)}

/* ── The two claims under the headline ─────────────────────────────────────────────────────────
   Was: a bold clause plus a mono uppercase letter-spaced descriptor, which read as a spec-sheet
   row. Now a statement and a plain qualifier, both in the same face, distinguished by weight and
   colour rather than by shouting. */
.claims{list-style:none; margin:var(--s6) 0 0; padding:0; border-top:1px solid var(--line)}
.claims li{
  display:flex; flex-wrap:wrap; align-items:baseline; gap:2px var(--s3);
  padding:var(--s4) 0; border-bottom:1px solid var(--line);
}
.claims .cn{font-size:var(--t-19); font-weight:var(--w-semi); letter-spacing:-.014em; color:var(--ink)}
.claims .cd{font-size:var(--t-14); color:var(--faint)}

/* ── The hero readout ──────────────────────────────────────────────────────────────────────────
   NOT A FLOATING CARD. The reference's cards sit on white with a soft shadow and that is its
   language; this site's is a framed instrument reading — a hairline, a flat white fill, no shadow,
   so it reads as a panel cut into the page rather than as an object hovering over it. */
.statcard{
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-lg);
  padding:clamp(20px,2.6vw,28px);
}
.statcard>.tag{display:block; margin-bottom:var(--s3)}
.statcard .sp-total{
  --od-h:1.16em;
  margin:0; font-weight:var(--w-bold); letter-spacing:-.032em;
  font-size:var(--t-num); line-height:1; color:var(--ink);
}
.statcard-k{margin:var(--s2) 0 0; font-size:var(--t-14); color:var(--faint); line-height:1.5}
.cstats{list-style:none; margin:var(--s5) 0 0; padding:0; border-top:1px solid var(--line)}
.cstats .cs{
  display:flex; align-items:baseline; justify-content:space-between; gap:var(--s2) var(--s4);
  padding:var(--s3) 0; border-bottom:1px solid var(--line-soft);
}
.cstats .cs:last-child{border-bottom:0; padding-bottom:0}
.cstats .k{font-size:var(--t-14); color:var(--faint); line-height:1.5}
.cstats .v{font-size:var(--t-16); font-weight:var(--w-semi); color:var(--ink); white-space:nowrap}
.cstats .is-you .k{color:var(--sub)}
.cstats .is-you .v{color:var(--lit)}
.stamp{margin:var(--s3) 0 0; font-size:var(--t-14); color:var(--faint)}

/* ── Section heads ─────────────────────────────────────────────────────────────────────────────
   Left-aligned, deliberately. The reference centres its section heads inside a 672px column, which
   suits a page that alternates copy and product shots; this page is a sequence of instruments and
   every one of them starts at the same left edge as its heading. */
.cmp-head{max-width:26em}
.cmp-head>.tag{display:block; margin-bottom:var(--s3)}
.cmp-more{margin:var(--s5) 0 0; font-size:var(--t-16); font-weight:var(--w-med)}
.cmp-more a{text-decoration:underline; text-underline-offset:3px; text-decoration-thickness:1px;
  text-decoration-color:rgba(20,24,26,.32)}
.cmp-more a:hover{text-decoration-color:var(--ink)}

/* ── Instrument 1: the distribution ────────────────────────────────────────────────────────────
   The rounded bordered box is gone. A number line does not need a container: it needs an axis, and
   drawing a box around it made it read as an empty input field waiting to be filled — which is
   exactly how the Firefox probe once photographed it.
   THE COMPARABLE BAND IS NEW and it is the substantive improvement. The narrowing from 24 to 5 was
   previously only asserted in a list underneath, so a reader had to take it on trust and then find
   five survivors among 24 near-identical ticks by eye. The band is drawn from the same pos()
   function that places the ticks (instrument.js), so the picture and the claim cannot disagree. */
.strip{margin-top:clamp(28px,3.4vw,40px)}
.strip-line{position:relative; height:92px}
/* The axis. A real rule the ticks stand on, so a gap in the data reads as a gap and not as a
   rendering failure. */
.strip-line::after{
  content:""; position:absolute; left:0; right:0; bottom:22px; height:1px; background:var(--line);
}
.strip-band{
  position:absolute; bottom:22px; top:14px; background:var(--lit-soft);
  border-left:1px solid var(--lit-line); border-right:1px solid var(--lit-line);
  border-radius:2px 2px 0 0; z-index:0;
}
/* Heights raised from 22/34/52 after looking at the render beside the reference: the strip read as
   a thin ruler with a small cluster in it rather than as a distribution, because the marks occupied
   about a quarter of the height the object reserved. The RATIOS are what carry the meaning, so all
   three moved together. */
.pt{
  position:absolute; bottom:22px; width:2px; height:30px; margin-left:-1px;
  background:#9aa3ae; border-radius:1px; transform-origin:bottom; z-index:1;
}
/* The five the methodology could NOT explain away. Taller and near-black, so the honest subset is
   visible without reading a word. */
.pt.is-comp{background:var(--ink); height:46px; width:2.5px}
.pt.is-you{
  background:var(--lit); width:4px; height:64px; margin-left:-2px; border-radius:2px; z-index:2;
  box-shadow:0 0 0 3px rgba(255,255,255,.9);
}
/* ⛔ THE TICKS ARE NEVER HIDDEN. NOT FOR A FRAME. They used to arm at scaleY(0) and grow in, and a
   Firefox probe photographed the exact frame that design permits: the number line as an empty box
   with its axis labels sitting under nothing, indistinguishable from a broken instrument. This is
   the most load-bearing object on the site; no growth animation is worth a state in which the
   product's headline evidence renders blank. */
html.anim .strip .pt{transform:none}
.strip-axis{
  list-style:none; margin:var(--s3) 0 0; padding:0;
  display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:var(--s2);
}
.strip-axis li{display:flex; flex-direction:column; gap:2px}
.strip-axis .sa-you{align-items:center}
.strip-axis .sa-hi{align-items:flex-end}
/* `is-aligned` is added by site.js only once it has read a real position off the you-tick, so the
   centred three-column floor is what renders with JS off. The label is centred ON the mark and
   clamped so an extreme position cannot push it out of the strip. */
/* TWO columns once the you-label is lifted out of the flow, not three. Taking an item out of a
   3-column grid leaves `.sa-hi` right-aligning inside column TWO, i.e. at 66% of the strip, so the
   "Highest" figure detached from the end of the axis it labels and crowded the you-label. */
.strip-axis.is-aligned{position:relative; grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
.strip-axis.is-aligned .sa-you{
  position:absolute; left:clamp(9%, var(--you-pos,50%), 91%); top:0;
  transform:translateX(-50%); align-items:center; white-space:nowrap;
}
.strip-axis .k{font-size:var(--t-14); color:var(--faint)}
.strip-axis .v{font-size:var(--t-16); font-weight:var(--w-semi)}
.strip-axis .sa-you .k{color:var(--sub)}
.strip-axis .sa-you .v{color:var(--lit)}
.strip-cap{margin:var(--s4) 0 0; font-size:var(--t-14); color:var(--faint); line-height:1.6}
/* Two captions under one figure are one caption block, not two footnotes. At the full s4 gap the
   shop table's ranking note and its lettering note read as unrelated remarks about different
   things, which is precisely what they are not. */
.strip-cap + .strip-cap{margin-top:var(--s2)}

/* ── The narrowing. A DISTINCT OBJECT, not a fifth copy of the ledger. ─────────────────────────
   This was four label/value rows plus a fifth, identical in construction to the three other lists
   on the page. Six instances of "mono label left, number right, hairline under" was the house
   template failure one layer down: one component doing every job, which is monotony, which reads as
   generated. It is now a horizontal reduction — three counts with the survivors carried forward —
   and it looks like nothing else on the site. */
.narrow{
  list-style:none; margin:clamp(24px,3vw,34px) 0 0; padding:0;
  display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:var(--s5);
  border-top:1px solid var(--line); padding-top:var(--s5);
}
.narrow li{display:flex; flex-direction:column; gap:var(--s1)}
.narrow .n{font-size:var(--t-24); font-weight:var(--w-bold); letter-spacing:-.02em; line-height:1.1}
.narrow .k{font-size:var(--t-14); color:var(--faint); line-height:1.5}
.narrow .is-keep .n{color:var(--lit)}
/* The payoff line: the comparable range and your position in it, given real weight. */
.verdict{
  margin:var(--s5) 0 0; padding:var(--s5) 0 0; border-top:1px solid var(--line);
  display:flex; flex-wrap:wrap; align-items:baseline; gap:var(--s2) var(--s5);
  font-size:var(--t-19); font-weight:var(--w-semi); letter-spacing:-.012em;
}
.verdict .vk{font-size:var(--t-14); font-weight:var(--w-reg); color:var(--faint)}
.verdict .over{color:var(--lit)}

/* ── Instrument 2: the ranked shop table ───────────────────────────────────────────────────────
   The most product-true object on the page and now the page's centrepiece. Every cell was 10.5px
   uppercase mono, which made a shop name and a price look like the same kind of thing; names are
   now text and prices are now tabular figures, which is the distinction the table exists to make.
   DELIVERY KEEPS ITS OWN COLUMN. That is not styling: the fixture's Halvorsen row has the lowest
   item price of the six and the highest total of the six because of one delivery charge, so a
   reader who compares the Total column picks it and loses money. The argument is the data. */
.shops{margin-top:clamp(26px,3.2vw,36px)}
.sr-head, .sr-rows{list-style:none; margin:0; padding:0}
.sr{
  display:grid; align-items:baseline;
  grid-template-columns:2.4ch minmax(0,1fr) 7.5rem 8rem 7.5rem;
  gap:var(--s1) var(--s4); padding:var(--s4) 0; border-bottom:1px solid var(--line);
}
/* The site's second and last uppercase role. A column header is the one place the device is a
   genuine convention rather than decoration. */
.sr-head .sr{
  padding:0 0 var(--s3); border-bottom:1px solid var(--line);
  font-size:var(--t-12); letter-spacing:.06em; text-transform:uppercase;
  color:var(--faint); font-weight:var(--w-bold);
}
.sr-head .sr span{font-size:inherit; font-weight:inherit; color:inherit}
.sr-n{font-size:var(--t-14); color:var(--faint)}
.sr-name{font-size:var(--t-16); font-weight:var(--w-semi); letter-spacing:-.01em; min-width:0}
.sr-item{font-size:var(--t-16); font-weight:var(--w-semi); color:var(--ink)}
.sr-ship{font-size:var(--t-16); color:var(--sub)}
.sr-total{font-size:var(--t-16); color:var(--sub)}
.sr.is-you{background:var(--lit-soft); box-shadow:inset 3px 0 0 var(--lit)}
.sr.is-you .sr-name{color:var(--lit)}
.sr.is-you>*:first-child{padding-left:var(--s3)}
.sr-tag{
  display:inline-block; margin-left:var(--s2); padding:2px 7px; border-radius:var(--r-sm);
  font-size:var(--t-12); font-weight:var(--w-med); color:var(--faint);
  border:1px solid var(--line); vertical-align:1px; white-space:nowrap; letter-spacing:0;
  text-transform:none;
}
.sr-win{color:var(--lit); border-color:var(--lit-line)}

/* ── The table's payoff. A callout, not a ledger. ──────────────────────────────────────────────
   Three rows of label/value underneath the table were a fourth copy of the same component. The
   trap the table sets deserves to be READ, so it is one framed statement. */
.trap{
  margin:clamp(24px,3vw,34px) 0 0; padding:clamp(18px,2.4vw,24px);
  border:1px solid var(--line); border-radius:var(--r-lg); background:var(--paper);
  display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--s5) clamp(24px,4vw,48px);
}
.trap>div{display:flex; flex-direction:column; gap:var(--s1)}
.trap .k{font-size:var(--t-14); color:var(--faint); line-height:1.5}
.trap .v{font-size:var(--t-19); font-weight:var(--w-semi); letter-spacing:-.014em}
.trap .is-best .v{color:var(--lit)}

/* ── Instrument 3: the decomposition ───────────────────────────────────────────────────────────
   ⛔ THE DARK PANEL IS GONE. It was the only dark surface on a light page, carried a lilac bar left
   over from a palette this site no longer uses, and sat in the middle of the page with nothing else
   like it — an orphan, and it read as a foreign object dropped in. The site now has exactly TWO
   dark moments and both are deliberate: the closing band and the prose pull-quote. */
.mfig{margin:0; padding:0; max-width:none}
.mfig .tag{display:block; margin-bottom:var(--s4)}
.mfig-bar{height:clamp(30px,3.4vw,40px); margin:0}
.mfig-rows{list-style:none; margin:var(--s5) 0 0; padding:0; max-width:none; border-top:1px solid var(--line)}
.mfig-rows li{
  display:grid; grid-template-columns:minmax(0,1fr) auto 4rem; gap:2px var(--s4);
  align-items:baseline; margin:0; padding:var(--s3) 0; border-bottom:1px solid var(--line-soft);
  line-height:1.4;
}
.mfig-rows li:last-child{border-bottom:0}
.mfig-rows .k{font-size:var(--t-16); color:var(--sub)}
.mfig-rows .v{font-size:var(--t-16); font-weight:var(--w-semi); color:var(--ink)}
.mfig-rows .p{font-size:var(--t-14); color:var(--faint); text-align:right}
.mfig-rows li[data-k="item"] .k{color:var(--ink); font-weight:var(--w-med)}
.mfig-rows .is-total{border-top:1px solid var(--line); border-bottom:0; margin-top:var(--s1); padding-top:var(--s3)}
.mfig-rows .is-total .k{color:var(--faint)}

/* The bar: one block that opens into three. The gap IS the idea — at gap 0 this is the single price
   the shop shows you; as it enters the viewport the same bar is visibly three parts. Segments use
   flex-grow (instrument.js) so the proportions hold at every gap with one set of numbers. */
.sp-bar{position:relative; display:flex; gap:var(--sp-gap,10px); height:clamp(30px,3.4vw,40px); margin:0}
.sp-seg{display:block; border-radius:var(--r-sm); min-width:3px; transform-origin:center; position:relative; z-index:1}
/* z-index:2 IS LOAD-BEARING. site.js writes a transform on each segment, and a transformed element
   paints in the positioned layer with static siblings behind it, so three later siblings once
   painted ON TOP of an overlay that was correctly at opacity 1 — the bar arrived already split with
   a stripe over most of it, which is a completely plausible picture and therefore exactly the kind
   of defect a screenshot pass approves. */
.sp-solid{position:absolute; inset:0; border-radius:var(--r-sm); opacity:0; pointer-events:none; z-index:2; background:var(--lit)}
.sp-seg[data-k="item"]{background:var(--lit)}
.sp-seg[data-k="delivery"]{background:#8b939d}
.sp-seg[data-k="tax"]{background:#c3c9d0}
html.anim .sp-seg{transition:opacity .45s ease}

/* ── The split section's two columns ───────────────────────────────────────────────────────────── */
.split-two{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,.92fr); gap:clamp(28px,4vw,56px); align-items:start}
/* The rules list. Fifth distinct treatment: these are RULES, not measurements, so they are set as
   readable statements with the operative half emphasised, not as a value column. */
.rules{list-style:none; margin:var(--s5) 0 0; padding:0; border-top:1px solid var(--line)}
.rules li{
  padding:var(--s3) 0; border-bottom:1px solid var(--line-soft);
  font-size:var(--t-16); line-height:1.55; color:var(--sub);
}
.rules li:last-child{border-bottom:0}
.rules b{color:var(--ink); font-weight:var(--w-semi)}

/* ── The citation. Sixth treatment, and the smallest. ──────────────────────────────────────────
   This was three rows of label/value — Source, Study, Data found — which gave a footnote the same
   visual weight as the product's own measurements. It is a citation; it is now set as one. */
/* FULL WIDTH, on purpose, and it is a composition fix rather than a decoration.
   Set as a narrow left-bordered block it left roughly 55% of the section as empty field beside four
   lines of text — the "one paragraph floating on a wash" shape this site has been rebuilt twice to
   get rid of. Its content is genuinely small (one finding, one source), so the answer is not to pad
   it out but to let it span, which also gives the page a wider beat between the two-column split
   above it and the closing band below. */
.cite-block{
  margin:clamp(24px,3vw,34px) 0 0; padding-top:var(--s5); border-top:1px solid var(--line);
  display:grid; grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);
  gap:var(--s4) clamp(28px,4vw,56px); align-items:baseline;
}
.cite-block .cw{font-size:var(--t-24); line-height:1.4; color:var(--ink); font-weight:var(--w-med); letter-spacing:-.016em; margin:0}
.cite-block .cs{margin:0; font-size:var(--t-14); color:var(--faint); line-height:1.6}

/* ── Instrument 4: SHOP LOCAL. The distance scale. ─────────────────────────────────────────────
   Added 2026-08-23. Andrew: *"users need the ability to filter by locality so they can shop local."*

   ⛔ WHY IT IS A BAR CHART AND NOT FOUR NUMBERS IN A ROW. Four independent counts side by side is
   the `.narrow` reduction wearing a different label, and this page has already been rebuilt once to
   stop one primitive doing every job. It is also the wrong reading: the tiers NEST, so what the
   filter actually produces is a set that grows as the circle widens, and a growing set is a bar.
   The page has no other bar chart. The tick strip is points on a number line; the decomposition is
   one bar cut into parts; this is four bars of increasing length. They cannot be confused.

   ⛔ THE RAMP IS TONAL, NOT ACCENTED, AND THAT IS A RULE BEING PROTECTED. Dark reads as near and
   pale reads as far, which is the natural encoding for distance and needs no key. Using the accent
   for "in your postal area" was the first draft and it was wrong: on this site --lit means "this is
   YOU" and nothing else, and a row about Shop D is not about you. The greys are the ones the
   decomposition bar already uses, so no new colour enters the sheet. The section's one accent
   moment is the payoff line, where "cheapest near you" genuinely is a fact about the reader. */
.rings{list-style:none; margin:clamp(28px,3.4vw,40px) 0 0; padding:0; border-top:1px solid var(--line)}
.rings li{
  display:grid; grid-template-columns:15rem minmax(0,1fr) 3ch;
  align-items:center; gap:var(--s4); padding:var(--s3) 0;
  border-bottom:1px solid var(--line-soft);
}
.rings li:last-child{border-bottom:0}
.rings .k{font-size:var(--t-16); color:var(--sub); line-height:1.45}
/* min-width guards the smallest tier from rendering as a sliver that reads as a rendering fault
   rather than as a small number. */
.rings .bar{display:block; height:10px; min-width:6px; border-radius:var(--r-sm); background:#dfe3e8}
.rings .n{
  font-size:var(--t-16); font-weight:var(--w-semi); color:var(--ink);
  text-align:right; font-variant-numeric:tabular-nums;
}
.rings [data-w="area"] .bar{background:#3d434b}
.rings [data-w="region"] .bar{background:#8b939d}
.rings [data-w="country"] .bar{background:#c3c9d0}

/* The payoff carries three pairs here rather than the two it carries under the distribution, so it
   gets a grid at narrow widths instead of the six stacked lines a flex column would produce. */
.verdict-3 .over{color:var(--lit)}

/* The guarantees. Same `.rules` treatment, laid out as a horizontal band instead of a vertical
   list, because the section above this one already uses the vertical form and two identical
   compositions back to back is exactly the sameness this build removed. */
.loc-rules{
  border-top:0; margin-top:clamp(28px,3.4vw,40px);
  /* ⛔ AN EVEN COLUMN COUNT, ALWAYS, AND `auto-fit` CANNOT GIVE ONE. The first version was
     `repeat(auto-fit,minmax(210px,1fr))`, which resolves to THREE columns at 900: the fourth
     guarantee dropped to a second row on its own, beside two empty cells, under a hairline that
     started and stopped over one column. It read as a layout fault rather than as a list.
     Four items want 4, 2 or 1 columns and nothing else, so the count is stated rather than solved
     for. 1100 is where four columns stop being wide enough for the longest of the four. */
  display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:0 clamp(24px,4vw,48px);
}
.loc-rules li{border-bottom:0; border-top:1px solid var(--line); padding:var(--s4) 0 0}

/* The locality of a shop, as the second line of its name cell in the ranked table. It is not a
   sixth column: the table is already the widest object on the site and the 768-1100 band is where
   it gives first. The swatch is `content:""` on purpose, so nothing is added to the page's text. */
.sr-loc{
  display:block; margin-top:3px; font-size:var(--t-14); font-weight:var(--w-reg);
  letter-spacing:0; color:var(--faint); line-height:1.4;
}
.sr-loc::before{
  content:""; display:inline-block; width:7px; height:7px; border-radius:2px;
  margin-right:7px; vertical-align:1px; background:#dfe3e8;
}
.sr-loc[data-w="area"]::before{background:#3d434b}
.sr-loc[data-w="region"]::before{background:#8b939d}
.sr-loc[data-w="country"]::before{background:#c3c9d0}
/* The accent on an is-you row belongs to the shop name, not to its locality line. */
.sr.is-you .sr-loc{color:var(--faint)}

/* ── `.cmp-ledger`, PROSE PAGES ONLY ───────────────────────────────────────────────────────────
   It left the home page with the four lists it used to build, but method.html and privacy.html
   still carry `class="ledger cmp-ledger"` and this rule going missing would have quietly changed
   two pages that were not in scope for this pass: the value column would have lost its right
   alignment and `.is-you` / `.is-total` would have lost their accent, on a page where `.is-you`
   marks the reader's own row. Kept in the NEW system (sans, tabular figures, the one accent)
   rather than restored as it was. */
.cmp-ledger>li{grid-template-columns:minmax(0,1fr) auto; align-items:baseline}
/* ⛔ NO `white-space:nowrap` HERE. I added it when restoring this rule, reasoning from the HOME
   page's short numeric values — but the home page no longer uses `.cmp-ledger` at all, and on
   privacy.html these values are long sentences. At 320/390 the row collapses to a single 1fr column
   and an unbreakable value pushed the document to 480px wide: a horizontal scrollbar on a site whose
   standing rule is that it may never scroll sideways. Caught by shoot.cjs's ten-width sweep, which
   exists because exactly this class of defect once hid between 390 and 1500 for four versions. */
.cmp-ledger .v{text-align:right}
.cmp-ledger .is-you .k, .cmp-ledger .is-you .v,
.cmp-ledger .is-best .k, .cmp-ledger .is-best .v{color:var(--lit)}
.cmp-ledger .is-total{border-bottom:0}
.cmp-ledger .is-total .k, .cmp-ledger .is-total .v{font-weight:var(--w-semi); color:var(--ink)}

/* ── Buttons ───────────────────────────────────────────────────────────────────────────────────── */
.btn{
  display:inline-flex; align-items:center; gap:var(--s2); min-height:46px;
  padding:var(--s3) var(--s5); border-radius:var(--r-md); text-decoration:none;
  font-weight:var(--w-semi); font-size:var(--t-16); letter-spacing:-.006em;
  background:var(--ink); color:#fff; border:1px solid var(--ink);
  transition:background .18s ease, border-color .18s ease, color .18s ease;
}
.btn:hover{background:#000; border-color:#000}
.btn .arw{font-size:var(--t-14); opacity:.7}
.btn-ghost{background:transparent; color:var(--ink); border-color:var(--line)}
.btn-ghost:hover{background:rgba(20,24,26,.05); color:var(--ink); border-color:var(--ink)}
.btnrow{display:flex; gap:var(--s3); flex-wrap:wrap; align-items:center}
.hero-cta{margin-top:var(--s6)}
.micro{margin:var(--s4) 0 0; font-size:var(--t-14); line-height:1.5; color:var(--faint); max-width:34em}
/* The collective line under the install buttons. It sits a step above the privacy micro because it
   is the campaign's claim rather than a footnote, and a step below the buttons because it is not
   the thing being clicked. */
.micro.join{margin-top:var(--s2); color:var(--ink); font-weight:var(--w-med)}

.stack a:not(.btn), .prose a:not(.btn), .ledger a:not(.btn){
  text-decoration:underline; text-underline-offset:3px; text-decoration-thickness:1px;
  text-decoration-color:rgba(20,24,26,.32);
}
.stack a:not(.btn):hover, .prose a:not(.btn):hover{text-decoration-color:var(--ink)}

/* ── The close. THE PAGE'S ONE SATURATED SET-PIECE. ────────────────────────────────────────────
   The measured reference spends its colour in exactly two places and leaves the other ~86% of the
   page white; this page does the same and puts its one moment HERE, where the page asks for the
   install. It is near-black rather than a colour wash, which is the opposite of the reference's
   aurora card and is the right register for a product about money. */
.closer{
  text-align:center; background:var(--dark); color:var(--dark-ink);
  border-radius:var(--r-lg); padding:clamp(44px,5.4vw,72px) clamp(24px,4vw,56px);
  margin-block:var(--sec);
}
.closer>.tag{display:block; margin-bottom:var(--s3); color:var(--dark-sub)}
.closer .h2{margin-bottom:var(--s5); color:#fff}
.closer .btnrow{justify-content:center; margin-top:var(--s6)}
.closer .btn{background:#fff; color:var(--ink); border-color:#fff}
.closer .btn:hover{background:var(--dark-ink); border-color:var(--dark-ink)}
.closer .btn-ghost{background:transparent; color:var(--dark-ink); border-color:var(--dark-line)}
.closer .btn-ghost:hover{background:rgba(255,255,255,.09); color:#fff; border-color:rgba(255,255,255,.4)}
.closer-facts{
  list-style:none; margin:0 auto; padding:0; max-width:720px;
  display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:0 clamp(24px,4vw,48px);
  border-top:1px solid var(--dark-line);
}
.closer-facts li{
  padding:var(--s3) 0; border-bottom:1px solid var(--dark-line); text-align:left;
  font-size:var(--t-16); line-height:1.5; color:var(--dark-sub);
}

/* ── Ledger (prose pages only now) ─────────────────────────────────────────────────────────────── */
.ledger{list-style:none; margin:0; padding:0; border-top:1px solid var(--line)}
.ledger>li{
  display:grid; grid-template-columns:11.5rem minmax(0,1fr); gap:var(--s1) var(--s5);
  padding:var(--s4) 0; border-bottom:1px solid var(--line); align-items:baseline; position:relative;
}
.ledger .k{font-size:var(--t-14); color:var(--faint); line-height:1.5}
.ledger .v{font-size:var(--t-16); font-weight:var(--w-semi); color:var(--ink)}
.ledger .d{margin:0; color:var(--sub); font-size:var(--t-16); line-height:1.55}
.ledger .d b{color:var(--ink); font-weight:var(--w-semi)}

/* ── Odometer internals ────────────────────────────────────────────────────────────────────────
   THE DEFINITE SLOT HEIGHT IS LOAD-BEARING and it is why this port measures nothing. The
   playground's odometer reads each column's clientHeight and writes it back onto that column's own
   children; with an indefinite height that is a feedback loop which multiplies the column by ten on
   every call, and it reached a stage 33,554,408px tall before anyone noticed, because a thumbnail
   shot timed out rather than looking wrong. Here the height, the glyph height and the translate
   distance are all `--od-h`, so the operation is idempotent. `overflow:hidden` on a fixed slot is
   what makes the row's width a constant of the digit COUNT: tabular figures alone are not enough,
   because a counter's digit count changes as it counts. */
.od{display:inline-flex; align-items:flex-start}
.od-fix{display:block; height:var(--od-h); line-height:var(--od-h)}
.od-col{display:block; overflow:hidden; width:.62em; height:var(--od-h)}
.od-strip{display:block; will-change:transform}
.od-strip span{display:block; height:var(--od-h); line-height:var(--od-h); text-align:center}
.vh{position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0}

/* ── Prose pages (method, privacy) ─────────────────────────────────────────────────────────────
   The prose column is constrained on the CHILDREN, not on `.wrap` itself: setting max-width on an
   element that is also a `.wrap` shrinks the centred box, so the body text re-centres and stops
   sharing a left edge with the header above it. Two different left margins on one page is the kind
   of thing nobody names and everybody feels. */
.wrap.prose{max-width:var(--maxw); counter-reset:sec}
.prose>section{
  padding:0; margin:0 0 clamp(36px,4.6vw,56px); position:relative;
  display:grid; grid-template-columns:var(--rail) minmax(0,1fr);
  gap:0 clamp(20px,3vw,44px); align-items:start; counter-increment:sec;
}
.prose>section>*{grid-column:2}
/* A CSS counter rather than markup, so a section cannot be added, moved or removed and leave a
   stale hand-typed index behind: there is no number in the HTML to drift. */
.prose>section::before{
  content:counter(sec, decimal-leading-zero); grid-column:1; grid-row:1;
  font-size:var(--t-14); font-weight:var(--w-semi); color:var(--faint); line-height:1.6; padding-top:6px;
  font-variant-numeric:tabular-nums;
}
.prose h2{font-size:clamp(22px,2.4vw,28px); line-height:1.16; letter-spacing:-.02em; font-weight:var(--w-bold); margin:0 0 var(--s4)}
.prose h3{font-size:var(--t-19); line-height:1.3; font-weight:var(--w-bold); margin:0 0 var(--s2); letter-spacing:-.012em}
.prose p, .prose ul{max-width:38em}
.prose p{line-height:1.72}
.prose ul{margin:0 0 1em; padding-left:1.1em; color:var(--sub)}
.prose li{margin-bottom:.45em; line-height:1.62}
.prose li::marker{color:var(--faint)}
/* The 38em clamp is for RUNNING TEXT. A ledger and a field grid are tables, and clamping them left
   the right quarter of the privacy page empty beside every row. Both opt out. */
.prose ul.deny, .prose>ul.deny{max-width:none}
.prose ul.ledger, .prose>ul.ledger{max-width:none}

/* The second and last dark moment on the site. */
.quote{
  margin:0; padding:clamp(22px,3vw,32px);
  background:var(--dark); color:var(--dark-ink); border-radius:var(--r-lg);
  font-size:clamp(17px,1.9vw,20px); line-height:1.5; letter-spacing:-.014em; font-weight:var(--w-semi);
  max-width:38em;
}
.quote cite{display:block; margin-top:var(--s4); font-size:var(--t-14); font-weight:var(--w-reg); color:var(--dark-sub); font-style:normal; letter-spacing:0}

.deny{list-style:none; margin:0; padding:0; display:grid; grid-template-columns:repeat(auto-fill,minmax(162px,1fr)); gap:var(--s1)}
/* ⛔ THE NAMES WRAP, THEY DO NOT TRUNCATE, and this is the one place monospace is earned. These two
   lists are the page's entire argument: the claim is that this is the COMPLETE set of fields, read
   out of the code that enforces it. An ellipsis in the middle of `is_marketplace_third_party`
   quietly turns a complete list into a partial one, on the one page where completeness is the
   point. A field name is a literal identifier and a monospace face is what makes it legible as one. */
.deny li{
  font-family:var(--mono); font-size:13px; line-height:1.35; padding:var(--s2) 10px;
  border-radius:var(--r-sm); background:var(--paper); color:var(--sub);
  border:1px solid var(--line); overflow-wrap:anywhere; margin:0;
}

/* ═══ THE MOTION LAYER ══════════════════════════════════════════════════════════════════════════
 * ⛔ THREE EFFECTS SURVIVE, DOWN FROM SIX, AND EVERY REMOVAL HAS A REASON.
 * The measured reference runs 11 of 14 effects as hover micro-state, exactly 3 scroll-driven, zero
 * keyframe animations and no pinned tracks. Andrew has now rejected the effects on five consecutive
 * versions, so the bar applied here was: would a careful designer defend this element in a
 * portfolio review?
 *
 * REMOVED — gradient-mask-hover (8.1). A 260px radial accent light that followed the cursor across
 *   every ledger row and the source card. On a page whose subject is money, a glow chasing the
 *   pointer over a data table is decoration pretending to be craft, and no portfolio review
 *   survives the question "why does the table light up?". Deleted with its plumbing, not disabled.
 * REMOVED — the specular sheen on `.btn` and `.shot-frame`. Same objection, and `.shot-frame` had
 *   additionally been dead since the product screenshot was unshipped: styling for an element the
 *   page no longer contains.
 * REMOVED — the `--mx`/`--my`/`--on` pointer plumbing entirely, so nothing writes a custom property
 *   on pointermove any more.
 * KEPT — the hero's transform-only arrival, the headline word stagger, and the below-fold reveal.
 * Everything below is either scoped to `html.anim` or inert until JS acts, so the no-JS and
 * reduced-motion floor is the COMPLETE page with nothing hidden and nothing frozen half way.       */

/* ⛔ THE HERO IS NEVER BELOW FULL OPACITY. NOT AT FIRST PAINT, NOT AT ANY FRAME.
 * site.js sits at the foot of the body, so the browser painted the complete hero, then JS added a
 * class which drove it to opacity 0 and it faded back in. Every reader saw a flash, and anything
 * sampling the page in that first second — a screenshot, a link preview, a slow phone, a starved
 * main thread — caught a half-faded hero. A fade is fine two screens down; on the first screen it
 * is the site failing to load. So: hero motion is TRANSFORM ONLY, it is a CSS animation rather than
 * a rAF loop (a keyframe animation cannot be starved, and `both` guarantees it ends on the final
 * frame), and nothing below the fold is armed if it is already on screen. */
@keyframes heroSettle{ from{transform:translateY(15px)} to{transform:none} }
@keyframes heroWord{   from{transform:translateY(.26em)} to{transform:none} }
html.anim .hero .ha{animation:heroSettle .66s cubic-bezier(.16,.84,.28,1) both; animation-delay:calc(var(--i,0) * 68ms)}

/* split-text-stagger (4.1). `display:inline-block` is what makes translateY apply to a word at all.
   The real sentence lives in one `aria-label` and every span is aria-hidden, because per-word spans
   otherwise fragment screen reader output into eight separate utterances. The shipped recipe
   animates opacity AND translateY; the opacity half is dropped for the reason above. */
.h1 .u{display:inline-block; white-space:pre}
html.anim .h1.is-split .u{animation:heroWord .58s cubic-bezier(.16,.84,.28,1) both; animation-delay:calc(var(--i,0) * 48ms)}

/* Section reveal, BELOW THE FOLD ONLY. site.js adds `rv`, and only to an element that is off screen
   when it arms, so nothing is hidden by a stylesheet nobody armed and nothing the reader is already
   looking at is hidden in order to be revealed to them. */
html.anim .rv{
  opacity:0; transform:translateY(16px);
  transition:opacity .5s ease, transform .62s cubic-bezier(.16,.84,.28,1);
  transition-delay:calc(var(--i,0) * 70ms);
}
html.anim .rv.is-in{opacity:1; transform:none}
main{position:relative}

/* The decomposition bar's floor is the SPLIT, complete: `.sp-solid` is transparent and `.sp-bar`
   carries its gap by default, so with JS off or under reduced motion the figure simply shows three
   parts and four real numbers. Only `html.anim` closes it up for the scroll to open again. */
html.anim .mfig .sp-bar{gap:0; transition:gap .82s cubic-bezier(.16,.84,.28,1) .12s}
html.anim .mfig .sp-solid{opacity:1; transition:opacity .82s ease .12s}
/* THEY REST AT .34, NOT AT .18, and the number was set off a still frame rather than by taste: at
   .18 a photograph of this figure mid-arrival showed four rows of barely-there grey text under one
   solid bar, which is the skeleton-loader read this site has shipped twice and been told about
   twice. Dimmed real text has to stay obviously text. */
html.anim .mfig .mfig-rows li{opacity:.34; transform:translateY(6px); transition:opacity .5s ease, transform .5s cubic-bezier(.16,.84,.28,1)}
html.anim .mfig.is-in .sp-bar{gap:10px}
html.anim .mfig.is-in .sp-solid{opacity:0}
html.anim .mfig.is-in .mfig-rows li{opacity:1; transform:none}
html.anim .mfig.is-in .mfig-rows li:nth-child(1){transition-delay:.26s}
html.anim .mfig.is-in .mfig-rows li:nth-child(2){transition-delay:.35s}
html.anim .mfig.is-in .mfig-rows li:nth-child(3){transition-delay:.44s}
html.anim .mfig.is-in .mfig-rows li:nth-child(4){transition-delay:.56s}

html.anim .prose>section.rv{opacity:0; transform:translateY(16px); transition:opacity .55s ease, transform .68s cubic-bezier(.16,.84,.28,1)}
html.anim .prose>section.rv.is-in{opacity:1; transform:none}

/* boop-spring-hover (8.2). The spring is in site.js; this is only the compositor hint. */
html.anim .btn{backface-visibility:hidden; will-change:transform}

/* ── Footer ────────────────────────────────────────────────────────────────────────────────────── */
footer.site{padding:var(--s7) 0 var(--s8); border-top:1px solid var(--line)}
.foot{display:flex; justify-content:space-between; gap:var(--s5); flex-wrap:wrap; align-items:flex-start}
.foot ul{list-style:none; margin:0; padding:0; display:flex; gap:var(--s5); flex-wrap:wrap}
.foot a{text-decoration:none; color:var(--sub); font-weight:var(--w-med); font-size:15px}
.foot a:hover{color:var(--ink); text-decoration:underline; text-underline-offset:3px}
.foot .note{max-width:31em}

/* ── Focus, for everyone ───────────────────────────────────────────────────────────────────────── */
:where(a,button,[tabindex]):focus-visible{outline:3px solid var(--lit); outline-offset:3px; border-radius:var(--r-sm)}
.closer :where(a,button):focus-visible{outline-color:#fff}

/* ── Responsive ────────────────────────────────────────────────────────────────────────────────── */
@media (max-width:1100px){
  :root{--rail:132px}
  /* THE 768-1100 BAND IS SAMPLED ON PURPOSE. A horizontal-overflow bug lived exactly here for four
     versions because every harness only shot 1500 and 390. The shop table is the widest fixed
     object on the site, so it is the one that has to give first. */
  .sr{grid-template-columns:2.4ch minmax(0,1fr) 6.5rem 7rem 6.5rem; gap:var(--s1) var(--s3)}
  .split-two{grid-template-columns:minmax(0,1fr) minmax(0,.95fr)}
  /* The distance labels are the longest fixed column in this band ("Elsewhere in your country"),
     so the label column gives before the bar does. */
  .rings li{grid-template-columns:12.5rem minmax(0,1fr) 3ch; gap:var(--s3)}
  /* 4 -> 2, never 3. See the note on the rule itself. */
  .loc-rules{grid-template-columns:repeat(2,minmax(0,1fr))}
  .loc-rules li:nth-child(n+3){margin-top:var(--s5)}
}
@media (max-width:900px){
  .hero-grid{grid-template-columns:1fr; gap:var(--s6)}
  .split{grid-template-columns:1fr}
  .split-two{grid-template-columns:1fr}
  .rail{grid-template-columns:1fr; gap:var(--s3)}
  .rail>.tag{padding-top:0}
  .prose>section{grid-template-columns:1fr; gap:var(--s1)}
  .prose>section>*{grid-column:1}
  .prose>section::before{grid-column:1; padding-top:0; margin-bottom:2px}
  .ledger>li{grid-template-columns:9.5rem minmax(0,1fr)}
}
@media (max-width:720px){
  /* ⛔ EVERY LABEL/VALUE PAIR STACKS. At 390 the citation block used to keep a two-column grid whose
     auto value column took its content width, squeezing "Data found in use to set prices" into a
     column one word wide — six lines, one word each, beside its value. A label that wraps per word
     is not a tight layout, it is a broken one. */
  .ledger>li{grid-template-columns:1fr; gap:var(--s1)}
  .narrow{grid-template-columns:1fr; gap:var(--s4)}
  .narrow li{flex-direction:row; justify-content:space-between; align-items:baseline; gap:var(--s4)}
  .narrow .n{font-size:var(--t-19)}
  .verdict{flex-direction:column; gap:var(--s1)}
  .trap{grid-template-columns:1fr; gap:var(--s4)}
  .cite-block{grid-template-columns:1fr; gap:var(--s3)}
  .cite-block .cw{font-size:var(--t-19)}
  /* THE SHOP TABLE DROPS ITS GRID AND BECOMES ROWS OF LABELLED PAIRS. A five-column table at 390px
     is either a horizontal scroller or four unreadable columns, and this site may never scroll
     sideways. The header row is hidden because its labels move into the cells via ::before. */
  .sr-head{display:none}
  .sr{grid-template-columns:auto minmax(0,1fr); gap:var(--s2) var(--s3); padding:var(--s4) 0; align-items:baseline}
  .sr-name{grid-column:2; font-size:var(--t-16)}
  .sr-n{grid-column:1; grid-row:1}
  .sr-item, .sr-ship, .sr-total{grid-column:1 / -1; display:flex; justify-content:space-between; gap:var(--s3)}
  .sr-item::before{content:"Item"}
  .sr-ship::before{content:"Delivery"}
  .sr-total::before{content:"Total"}
  .sr-item::before, .sr-ship::before, .sr-total::before{
    font-size:var(--t-14); color:var(--faint); font-weight:var(--w-reg);
  }
  .sr.is-you>*:first-child{padding-left:var(--s2)}
  .strip-line{height:80px}
  .pt{height:26px}
  .pt.is-comp{height:40px}
  .pt.is-you{height:56px}
  /* The axis stacks into labelled rows at this width, so the absolute alignment is switched OFF
     rather than left to position a row on top of another one. */
  /* ⛔ BOTH SELECTORS, and the doubled one is load-bearing. `.strip-axis.is-aligned` is specificity
     (0,2,0) and a bare `.strip-axis` here is (0,1,0), so the desktop two-column rule OUTRANKS this
     one even though it sits inside a narrower media query: a media query adds no specificity. The
     axis rendered as two columns at 390 with "Lowest / You" sharing a row and "Highest" orphaned
     below. Same trap as the media-query override ORDER note in the scroll-figure pattern entry. */
  .strip-axis, .strip-axis.is-aligned{grid-template-columns:1fr; gap:var(--s3)}
  .strip-axis.is-aligned{position:static}
  .strip-axis.is-aligned .sa-you{position:static; transform:none}
  .strip-axis .sa-you, .strip-axis .sa-hi{align-items:flex-start}
  .strip-axis li{flex-direction:row; justify-content:space-between; align-items:baseline}
  /* The distance scale keeps its bars at 390: dropping the chart and leaving label/value pairs
     would turn the one figure in this section back into the ledger it was written not to be. The
     label and the count share the first row, the bar spans the full width underneath. */
  .rings li{grid-template-columns:minmax(0,1fr) 3ch; gap:var(--s2) var(--s3); padding:var(--s4) 0}
  .rings .k{grid-column:1; grid-row:1}
  .rings .n{grid-column:2; grid-row:1}
  .rings .bar{grid-column:1 / -1; grid-row:2}
  /* One column, and the two-column row gap from the 1100 block is RESET rather than inherited: both
     media queries match at 390, so without this items 3 and 4 carry a 24px margin the first two do
     not, in a single column where every gap should be identical. */
  .loc-rules{grid-template-columns:1fr}
  .loc-rules li:nth-child(n+3){margin-top:0}
  /* Three label/value pairs as a flex column would be six alternating lines. A two-column grid
     keeps each label with its own value, which is what the pairs are for. */
  .verdict-3{
    display:grid; grid-template-columns:auto auto; justify-content:space-between;
    align-items:baseline; gap:var(--s2) var(--s4);
  }
  .closer-facts{grid-template-columns:1fr}
  .claims li{gap:2px var(--s3)}
  nav.site ul{gap:var(--s4)}
  .hero-cta .btn{flex:1 1 auto; justify-content:center}
  .mfig-rows li{grid-template-columns:minmax(0,1fr) auto}
  .mfig-rows .p{display:none}
}

/* Belt and braces: if the OS asks for reduced motion, nothing transitions even if a class is stale. */
@media (prefers-reduced-motion: reduce){
  *{animation:none !important; transition:none !important; scroll-behavior:auto !important}
}
