/* =============================================================================
   guy flexor — design system and page styles
   -----------------------------------------------------------------------------
   Guy Flexor is a PUBLICATION that earns affiliate commission. It is not a shop.
   It never ships, stocks, prices or refunds anything (STATUS.md §0). Nothing in
   this file may make it look like a storefront: no cart, no price badges, no
   "buy now", no compare-at strikethroughs, no urgency furniture.

   THE READER, and why the numbers below are what they are
   -------------------------------------------------------
   Men 45-65 who are getting back into it. Presbyopia is near-universal past 45,
   so body text is 18px minimum and the article measure is set at 19-20px with
   1.7 leading. This reader has been marketed to for forty years, so the page is
   built to look calm and expensive rather than clever. Phone first, one-handed.

   WHAT CHANGED FROM THE EXISTING BRAND ATOMS, AND WHY
   ---------------------------------------------------
   Starting atoms (favicon.svg): #0a0a0a near-black, #ffffff white, #14cfc6 teal.
   All three survive. Three deliberate evolutions, each with a reason:

   1. The page ground moves from #ffffff to #fbfaf8, a warm off-white.
      Reason: a full-bleed pure white at the brightness this reader runs a phone
      at causes halation around serif stems, and white reads as "app" where warm
      paper reads as "printed". #0a0a0a on #fbfaf8 measures 18.98:1.

   2. Reading text moves from #0a0a0a to #1c1b19 (16.50:1 on paper).
      Reason: maximum contrast is not maximum legibility for long-form. #0a0a0a
      is retained for display type and the mark, so the brand atom stays exactly
      where it is actually used.

   3. The teal gains a text-safe companion, --teal-ink #0b6f6a (5.76:1 on paper).
      Reason: #14cfc6 measures 1.87:1 on paper. It fails AA text contrast AND the
      3:1 non-text floor, so it can never be a link, a label or a focus ring on a
      light ground. It stays what it already was in favicon.svg — a mark. On the
      dark footer it is 10.15:1 and may be text there.

   Every ratio quoted in this file was COMPUTED, not estimated. The calculator is
   test/contrast-check.js; run it after changing any colour token.

   MOTION PERSONALITY: steady, deliberate, unhurried.
   Studied from ~/3d-portfolio (technique only — no code, no stack taken). Its
   reveals are one-shot, never replayed, which is right. Its 1s durations, 50px
   lifts and 200-300ms staggers are not: on a six-item list that is nearly two
   seconds before the last item exists. Halved throughout. It also ships no
   prefers-reduced-motion and no hover gating at all; both are built in here.

   FAIL-OPEN RULE, and it is load-bearing
   --------------------------------------
   Reveal animations hide their element until an observer fires. If the observer
   never runs, the content is permanently invisible — and on this site some of
   that content is a legally required disclosure. So .reveal only hides anything
   when the root element carries .has-js, which an inline head script sets before
   first paint. No JS, a script error, a parse failure: nothing is hidden.
   The disclosure is additionally never placed inside a reveal container, and
   test/disclosure-check.js asserts that against the HTML.
   ========================================================================== */

/* ------------------------------------------------------------------- fonts */

/* Self-hosted, latin subset only, from the Google Fonts CDN's own subsetting.
   Both faces are SIL OFL 1.1 — see fonts/OFL.txt. Sizes as shipped:
     source-serif-4-latin.woff2         50.8 KB  wght 400-700, optical size pinned
     source-serif-4-latin-italic.woff2  20.1 KB  wght 400
     archivo-latin.woff2                34.9 KB  wght 400-800
   The two roman faces are preloaded in the document head; the italic is not,
   because no italic appears above the fold on either template.

   THE PAIRING, and the Archivo decision the brief asked for:
   Archivo is KEPT, and demoted. favicon.svg is a heavy italic Archivo "g", so
   dropping it would orphan the mark. But Archivo is a grotesque, and a grotesque
   set at 19px for 1,500 words is tiring for a presbyopic reader, and a grotesque
   headline over grotesque body has no hierarchy except size. So Archivo now does
   structure — masthead, headings, labels, buttons, metadata — and Source Serif 4
   does all reading. Source Serif 4 was chosen over Literata/Newsreader/Spectral
   because it is a transitional text face designed for screens with a large
   x-height and open counters, its italic is a true italic rather than a slant,
   and it is quiet: it reads as a broadsheet, not as a brand. */

@font-face {
  font-family: "Source Serif 4";
  src: url("fonts/source-serif-4-latin.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Source Serif 4";
  src: url("fonts/source-serif-4-latin-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: Archivo;
  src: url("fonts/archivo-latin.woff2") format("woff2");
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ------------------------------------------------------------------ tokens */

:root {
  /* --- colour ------------------------------------------------------------
     Measured against the ground each is used on. Recompute with
     test/contrast-check.js before changing any value here. */

  --ink: #0a0a0a;          /* display type, the mark      18.98:1 on paper */
  --ink-body: #1c1b19;     /* all reading text            16.50:1 on paper */
  --ink-soft: #55524d;     /* metadata, captions, dates    7.46:1 on paper */

  --paper: #fbfaf8;        /* page ground */
  --paper-sunk: #f1efea;   /* qualifying blocks, table heads, bands */
  --rule: #ddd8cf;         /* hairlines. Non-text: 1.4:1 is deliberate —
                              a rule that shouts is a rule you read. */
  --rule-strong: #b8b2a7;  /* rules that carry structural meaning */

  --teal: #14cfc6;         /* THE MARK. Structure only on light grounds:
                              1.87:1 on paper. Never text, never a lone
                              focus ring. On --ink it is 10.15:1 and is text. */
  --teal-ink: #0b6f6a;     /* text-safe teal               5.76:1 on paper */

  /* SETTLED 2026-08-31 — one value, and the brand files now carry it too.
     Two agents derived a light-ground teal in parallel on the same day. Both
     applied the same rule and both did the arithmetic correctly. They
     disagreed only about which grounds they measured against:

                    on #ffffff   on --paper   on --paper-sunk
       #0E7E77         4.92         4.72         4.28   <- FAILS AA on the band
       #0b6f6a         6.01         5.76         5.23   <- clears every ground

     #0E7E77 was computed against pure #ffffff, the canonical reference ground
     and one this site never paints. The page is a warm off-white and
     qualifying blocks sit on a warmer band again, and THAT BAND is where the
     affiliate disclosure, the who-should-skip block and the evidence-grade
     chips live. A value that clears on paper and fails on the band would have
     shipped looking verified.

     So the brand note's own rule — "the ground decides, not taste" — is
     upheld rather than overturned. It simply has to enumerate EVERY ground
     the token can land on. #0b6f6a stays, and brand/logo.svg and
     brand/logo-mark-light.svg were changed to match it: the mark's rule sits
     inches from link text in the masthead, and two teals six percent apart in
     luminance side by side read as a rendering fault, not a palette.

     test/contrast-check.js asserts --teal-ink on BOTH grounds, so any future
     value clearing only one fails the run rather than failing a reader.
     test/brand-check.js asserts the inlined mark's accent still equals this
     token, so the stylesheet and the SVG cannot drift apart again. */
  --teal-deep: #0fb3ab;    /* the disclosure border. Retained atom. */

  --caution: #8a1c10;      /* skip-section accent          8.93:1 on paper */

  /* Retained aliases so nothing that predates this system breaks. */
  --volt: var(--teal);
  --volt-deep: var(--teal-deep);
  --fog: var(--paper-sunk);

  /* --- type --------------------------------------------------------------
     Major-third-ish scale, fluid where it earns it. --fs-md is the floor for
     anything a reader reads in sentences: 18px, never lower.
     --fs-xs (15px) is the disclosure floor — see the disclosure block. */

  --font-text: "Source Serif 4", Charter, "Bitstream Charter", Cambria,
    "Times New Roman", Georgia, serif;
  --font-ui: Archivo, "Helvetica Neue", Inter, system-ui, Arial, sans-serif;

  --fs-3xs: 12px;                                  /* tracked labels ONLY */
  --fs-2xs: 14px;                                  /* fine print */
  --fs-xs: 15px;                                   /* captions, metadata */
  --fs-sm: 17px;
  --fs-md: clamp(18px, 1.05rem + 0.2vw, 19px);     /* body */
  --fs-article: clamp(18.5px, 1.05rem + 0.35vw, 20px); /* article body */
  --fs-lg: clamp(20px, 1.1rem + 0.55vw, 23px);     /* standfirst */
  --fs-xl: clamp(23px, 1.2rem + 0.9vw, 28px);      /* h3 */
  --fs-2xl: clamp(27px, 1.3rem + 1.6vw, 36px);     /* h2 */
  --fs-3xl: clamp(33px, 1.5rem + 2.6vw, 50px);     /* h1 */
  --fs-4xl: clamp(40px, 1.5rem + 5.4vw, 84px);     /* hero display */

  --lh-tight: 1.06;
  --lh-head: 1.18;
  --lh-body: 1.6;
  --lh-read: 1.7;                                  /* article prose */

  --track-label: 0.13em;                           /* small-caps labels */
  --track-display: -0.02em;                        /* display type only */

  /* --- space -------------------------------------------------------------
     4px base. Editorial pages live in the top half of this scale. */

  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
  --sp-8: 64px;
  --sp-9: 96px;
  --sp-10: 128px;

  --gutter: clamp(20px, 5vw, 64px);
  --band: clamp(56px, 8vw, 112px);                 /* section vertical rhythm */

  /* --- measure -----------------------------------------------------------
     40rem at 19-20px lands at 66-70 characters, which is the range this
     reader's eye tracks without losing the line return. */

  --measure: 40rem;        /* 640px — article prose */
  --measure-wide: 52rem;   /* 832px — standfirsts, tables, figures */
  --page: 76rem;           /* 1216px — outer page */

  /* --- motion ------------------------------------------------------------
     Steady, deliberate, unhurried. One decelerating curve does almost all the
     work; the in-out curve exists only for things that move and come back. */

  --dur-press: 110ms;
  --dur-hover: 190ms;
  --dur-ui: 300ms;
  --dur-reveal: 560ms;

  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);

  --reveal-lift: 12px;
  --stagger: 90ms;

  /* --- focus -------------------------------------------------------------
     Two-tone. The ink outline carries the contrast (18.98:1) because raw teal
     on paper is 1.87:1 and would fail the 3:1 non-text floor on its own; the
     teal halo is recognition, not legibility. Dark sections override --focus. */

  --focus: var(--ink);
  --focus-halo: rgba(20, 207, 198, 0.38);
}

/* BREAKPOINTS — documentation only. Custom properties cannot be used inside a
   media query, so these values appear as literals below. Keep them in sync.
     sm  480px   phone landscape
     md  768px   the single biggest layout change: columns collapse
     lg 1040px   article gains its margin column
     xl 1280px   page hits --page and stops growing                       */

/* -------------------------------------------------------------- reset/base */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink-body);
  font-family: var(--font-text);
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img,
video,
svg {
  display: block;
  max-width: 100%;
}

/* An author-level `display: block` on <video> beats the user agent's
   `[hidden] { display: none }`, because author styles win over UA styles
   regardless of specificity. Without this the hero video would paint even
   while it has no source. */

[hidden] {
  display: none !important;
}

img,
video {
  height: auto;
}

a {
  color: inherit;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-ui);
  color: var(--ink);
  line-height: var(--lh-head);
  letter-spacing: -0.012em;
  text-wrap: balance;
  margin: 0;
}

p {
  margin: 0;
  text-wrap: pretty;
}

ul,
ol {
  margin: 0;
}

/* Focus. Never removed, only restyled. :focus-visible so a mouse click on a
   button does not paint a ring, but every keyboard and switch user gets one. */

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  box-shadow: 0 0 0 6px var(--focus-halo);
  border-radius: 1px;
}

/* Skip link. Visible the moment it is focused; never a 1px trap. */

.skip-link {
  position: absolute;
  left: var(--sp-4);
  top: var(--sp-4);
  z-index: 100;
  transform: translateY(-160%);
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  font-weight: 600;
  padding: var(--sp-3) var(--sp-5);
  text-decoration: none;
  transition: transform var(--dur-ui) var(--ease-out);
}

.skip-link:focus-visible {
  transform: none;
}

/* -------------------------------------------------------------- primitives */

.wrap {
  max-width: var(--page);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.measure {
  max-width: var(--measure);
}

.measure-wide {
  max-width: var(--measure-wide);
}

.section {
  padding-block: var(--band);
}

.section--sunk {
  background: var(--paper-sunk);
}

.section--ink {
  background: var(--ink);
  color: #c9c6c0;                                /* 11.62:1 on ink */
  --focus: var(--paper);
  --rule: #2b2a28;
  --rule-strong: #46443f;
  --ink-soft: #a8a49d;                           /*  7.98:1 on ink */
}

.section--ink h1,
.section--ink h2,
.section--ink h3,
.section--ink h4 {
  color: var(--paper);
}

.rule {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: 0;
}

/* A small-caps label. Archivo, tracked, never used for a sentence. It is the
   only place --fs-3xs (12px) is allowed, because it is a signpost, not text. */

.label {
  font-family: var(--font-ui);
  font-size: var(--fs-3xs);
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 0;
}

.label--teal {
  color: var(--teal-ink);
}

.section--ink .label--teal {
  color: var(--teal);                            /* 10.15:1 on ink */
}

/* SECTION HEADS.
   The previous version of this was a flex row: big heading on the left, small
   label floated to the right on the same baseline, hairline underneath. It was
   on every section of every page.

   That composition is the single most reliable tell that a page was assembled
   rather than designed. It appears on essentially every generated marketing
   site, it gives each section the identical rhythm so the eye stops reading
   the hierarchy after the second one, and the right-hand text is almost always
   a caption nobody asked for, floating with no alignment to anything below it.

   So the head is now STACKED and the right-hand slot is gone. A section is
   allowed a small-caps label, but it sits above the heading where it reads as
   a kicker, and DESIGN-CONTRACT.md rations them to at most one per three
   sections. The heading alone is nearly always enough: a section's position on
   the page already says what it is.

   The rule below the heading is the only thing that varies, and it varies with
   what the section is for rather than decoratively:
     default      1px hairline   a heading, not an announcement
     --major      2px ink        the section a reader came for
   Two weights, both meaningful. A third would be decoration. */

.section__head {
  margin-bottom: var(--sp-7);
}

.section__head > .label {
  margin-bottom: var(--sp-3);
}

.section__head h2 {
  font-size: var(--fs-2xl);
  font-weight: 700;
  letter-spacing: var(--track-display);
  max-width: 22ch;
  padding-bottom: var(--sp-4);
  border-bottom: 1px solid var(--rule-strong);
}

.section__head--major h2 {
  border-bottom-width: 2px;
  border-bottom-color: var(--ink);
}

.section--ink .section__head--major h2 {
  border-bottom-color: var(--rule-strong);
}

/* A standfirst under a section heading, for the two sections that genuinely
   need a sentence of orientation. It sits UNDER the heading at reading width,
   never beside it in a floating right-hand column. */

.section__note {
  margin-top: var(--sp-5);
  max-width: 52ch;
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  color: var(--ink-soft);
}

/* ==========================================================================
   THE BRAND MARK  —  integrated 2026-08-31
   --------------------------------------------------------------------------
   The mark itself is designed in brand/ and documented in brand/INTEGRATION.md
   and brand/motion_spec.md. This block sizes it, grounds it, and carries the
   choreography, which was inlined from brand/motion.css so the site ships one
   stylesheet and no extra request.

   HOW THE MARK GETS INTO THE PAGE
   The SVG is inlined into the markup by tools/mark-inline.js, between
   <!-- MARK:START --> and <!-- MARK:END -->, and test/brand-check.js asserts
   the copies still match brand/logo.svg. It has to be inline: an
   <img src="logo.svg"> is an isolated document, so no external rule can reach
   inside it for currentColor, for the ground-aware accent, or for motion.

   TWO ASSETS, ONE LOCKUP AND ONE COMPACT G, AND WHY BOTH SHIP
   The lockup's verified legibility floor is 180px wide (brand/INTEGRATION.md
   §2). On a 390px phone with a 20px gutter there are 350px of line, and 180px
   of lockup plus three nav links does not fit. The previous answer was a
   two-row masthead, which cost ~44px of a short viewport on every screen of
   every article. The compact G exists precisely for this and holds down to
   16px, so the phone masthead uses it and the desktop masthead uses the
   lockup. Both are in the markup; exactly one is ever displayed.

   COLOUR IS NOT IN THE MARKUP
   The inlined SVG carries currentColor on the letterforms and no colour at all
   on the accent parts. So the mark inherits ink on paper and paper on ink from
   whatever section it sits in, and its accent IS --teal-ink rather than a copy
   of it. A palette change reaches the logo; it cannot leave the logo behind.

   SIZES ARE FIXED, NOT FLUID
   A fluid box changes height with the viewport and is the easiest way to spend
   a CLS budget of 0.1. Every box below keeps the lockup's 4.41:1 ratio.
   ========================================================================== */

.mark {
  display: inline-flex;
  align-items: center;
  flex: none;
  text-decoration: none;
  color: var(--ink);
  /* The mark is a 44px-tall touch target at minimum even where the artwork
     is shorter, so a hand that is not perfectly steady still hits it. */
  min-height: 44px;
}

/* OPTICAL ALIGNMENT, and it is not a nicety.
   Both viewBoxes carry the mark's clear space inside them: the lockup starts
   at x=-26 of 1080 units, the compact mark at x=-8 of 120. Rendered, that is
   5 to 9 real pixels of transparent padding on the left edge. Left alone, the
   wordmark's stem sits inside the page gutter while every heading, paragraph
   and nav link below it sits on the gutter, and the masthead reads as very
   slightly adrift from its own page for a reason nobody can name.

   So each size pulls its own padding back out. The values are exact rather
   than eyeballed: inset = 26/1080 x width for the lockup, 8/120 x width for
   the compact mark. The clear space still exists -- it is enforced by layout
   spacing around .mark, which is what INTEGRATION.md asks for -- it just is
   not counted twice.

   THE WIDTHS CHANGED WITH THE MARK ON 2026-08-31, AND NOT BY TASTE.
   The mark that shipped carries 100 units of cap height in a 1080-unit box.
   The retired one carried 100 in 911. So the same pixel width now yields a
   SMALLER letter: 268px used to give a 29.4px cap and would now give 24.8px.
   What a reader perceives is cap height, not lockup width, so each width below
   is the one that holds cap height where it was:

       268 x (1080/911) = 318      desktop
       320 x (1080/911) = 379      homepage masthead

   Verified in brand/proof/03-masthead-sizes.png at 1:1, and on the rendered
   page at 390px, 768px and 1440px. The lockup is wider than it was, which is
   what a plainer, more widely spaced wordmark costs; below 768px the compact
   mark carries the masthead instead, which is what it is for. */

.mark__lockup {
  display: none;
  width: 254px;
  height: 49.3px;
  margin-left: -6.1px;                 /*  26/1080 x 254 */
}

.mark__compact {
  display: block;
  width: 40px;
  height: 40px;
  margin-left: -2.7px;                 /*  8/120 x 40   */
}

@media (min-width: 768px) {
  .mark__lockup { display: block; width: 318px; height: 61.8px; margin-left: -7.7px; }
  .mark__compact { display: none; }
}

/* The homepage masthead is a real masthead and the article pages get a running
   head. That is a publication convention, not decoration: the front of a paper
   announces itself, the inside pages get out of the way of the text. */

/* The homepage size moved from the 768px breakpoint to 1100px. At 768 the wrap
   is 691px of content and a 379px lockup plus four nav links does not fit
   without the nav wrapping under the mark, which turns the masthead into two
   rows and eats a quarter of a tablet's first screen. Between 768 and 1100 the
   homepage therefore gets the standard 318px lockup, which is still larger in
   cap height than anything that shipped before today. */

@media (min-width: 1100px) {
  .masthead--home .mark__lockup { width: 379px; height: 73.6px; margin-left: -9.1px; }
}

/* Ink grounds. The letterforms follow `color`, and so does everything else,
   because the mark is now monochrome.

   WHAT WAS DELETED HERE, SO IT IS NOT RE-ADDED BY SOMEONE READING A STALE
   NOTE. Six rules used to set .gf-rule, .gf-dot and .gf-bar to --teal-ink on
   paper and switch them to --teal on ink. The mark they were written for
   carried an accent inside the letterforms. The mark that replaced it does not
   and may not: the accent is a dark-ground interface colour, it never carries a
   letterform on any ground, and brand/build.py fails the build if it appears in
   the artwork at all. See brand/INTEGRATION.md section 4a -- five designers
   rediscovered this constraint independently and each wasted work on it.

   Every part of the mark is now currentColor, so this one rule is the whole
   ground-awareness system. */

.section--ink .mark,
.foot .mark {
  color: var(--paper);
}

/* ---- the choreography ---------------------------------------------------
   Inlined from brand/motion.css, unchanged in substance. Every number is
   derived in brand/motion_spec.md rather than chosen by taste, and the
   personality is steady, deliberate, unhurried.

   The safety default is that an SVG with no context class does not move at
   all. Motion is opt-in per context, so the failure mode of forgetting a
   class is a static logo and never a hidden wordmark.

   THE ONE RULE THAT MATTERS MOST HERE: the wordmark is opaque at t=0 and is
   never animated in a masthead. On a publication the name may not wait for an
   animation, and text that is invisible until a script or a compositor
   cooperates is text that is sometimes not there at all. Only the rule and
   the dot move.

   Two mechanical rules carried over from pixel2motion, both load-bearing:
   every timing function inside @keyframes is a literal cubic-bezier, because
   a var() there is silently dropped by Chromium and the segment degrades to
   linear; and the dash setup selector is scoped to exactly the parts the
   active context draws, because stroke-dasharray:1 on a path without
   pathLength means one user unit and renders as stripes rather than as a
   draw-on.                                                                 */

:root {
  --p2m-duration-header: 700ms;   /* Trustworthy preset base */
  --p2m-duration-hover: 220ms;    /* Timing table 150-300ms  */
  --gf-hover-step: 6px;           /* how much further the line goes */
}

@keyframes gf-rule-draw {
  0%, 10% {
    stroke-dashoffset: 1;
    animation-timing-function: cubic-bezier(0.34, 0, 0.14, 1); /* deliberate */
  }
  76%, 100% { stroke-dashoffset: 0; }
}

@keyframes gf-dot-in {
  0%, 70% {
    opacity: 0;
    transform: scale(0.85);
    animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);   /* settle */
  }
  96%, 100% { opacity: 1; transform: scale(1); }
}

@keyframes gf-bar-draw {
  0%, 12% {
    stroke-dashoffset: 1;
    animation-timing-function: cubic-bezier(0.34, 0, 0.14, 1);
  }
  82%, 100% { stroke-dashoffset: 0; }
}

@media (prefers-reduced-motion: no-preference) {
  /* Scoped to exactly the parts each context draws. The lockup also contains
     a .gf-bar -- the G's crossbar -- which must NOT be caught by this, both
     because two lines drawing at once splits the staging and because in the
     lockup that path is a letterform rather than the accent. */
  .gf-mast .gf-rule,
  .gf-mark .gf-bar { stroke-dasharray: 1; }

  .gf-mast .gf-dot { transform-box: fill-box; transform-origin: center; }

  .gf-mast .gf-rule { animation: gf-rule-draw var(--p2m-duration-header) both; }
  .gf-mast .gf-dot { animation: gf-dot-in var(--p2m-duration-header) both; }

  /* The compact mark is the phone masthead, so it gets the same device at
     icon scale: one line, drawn once, left to right. */
  .gf-mark .gf-bar { animation: gf-bar-draw var(--p2m-duration-header) both; }

  /* Hover and focus. "The line goes a little further." A response, not a
     performance: the rule and the dot travel together so the gap between them
     never changes. Gated behind a real pointer, because on a touch device
     :hover latches on tap and the mark would sit in its hover state. */
  @media (hover: hover) and (pointer: fine) {
    .gf-hover .gf-rule,
    .gf-hover .gf-dot {
      transition: transform var(--p2m-duration-hover) cubic-bezier(0, 0, 0.2, 1);
    }
    .gf-hover .gf-rule { transform-box: fill-box; transform-origin: left center; }

    .gf-hover:hover .gf-rule { transform: scaleX(1.0072); }
    .gf-hover:hover .gf-dot { transform: translateX(var(--gf-hover-step)); }
  }

  /* Focus is not gated behind a pointer -- a keyboard user gets the response
     too, and it rides on top of the focus ring rather than replacing it. */
  .gf-hover:focus-visible .gf-rule,
  .gf-hover:focus-visible .gf-dot {
    transition: transform var(--p2m-duration-hover) cubic-bezier(0, 0, 0.2, 1);
  }
  .gf-hover:focus-visible .gf-rule {
    transform-box: fill-box;
    transform-origin: left center;
    transform: scaleX(1.0072);
  }
  .gf-hover:focus-visible .gf-dot { transform: translateX(var(--gf-hover-step)); }
}

/* Reduced motion: the finished logo, immediately. Not a shorter animation --
   no animation. For a 45-65 audience vestibular sensitivity is a median
   concern rather than an edge case. */
@media (prefers-reduced-motion: reduce) {
  .gf-mast .gf-rule,
  .gf-mast .gf-dot,
  .gf-mark .gf-bar {
    animation: none !important;
    transition: none !important;
    stroke-dashoffset: 0 !important;
    opacity: 1 !important;
    transform: none !important;
  }
}

/* ---------------------------------------------------------------- masthead */

/* Deliberately NOT fixed. A fixed bar costs 56-64px of a phone's already short
   viewport on every screen of a 2,000-word article, and it is the single most
   common way a content site starts to feel like an app.

   The phone masthead is now ONE row, because the compact mark fits where the
   lockup did not. That returns about 44px of viewport on every screen of every
   article and removes the borrowed-hairline second row, which was the weakest
   piece of chrome on the site.

   The rule under the masthead is 2px of ink on the homepage and 1px of hairline
   elsewhere. On a publication the front page is announced and the inside pages
   get out of the way; a single weight everywhere is the flatness that makes a
   site read as a template. */

.masthead {
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
}

.masthead--home {
  border-bottom-width: 2px;
  border-bottom-color: var(--ink);
}

.masthead__in {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  min-height: 64px;
  padding-block: var(--sp-3);
}

@media (min-width: 768px) {
  .masthead__in { min-height: 80px; gap: var(--sp-5); }
  .masthead--home .masthead__in { min-height: 104px; }
}

.masthead__nav {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: clamp(var(--sp-4), 2.5vw, var(--sp-6));
}

.masthead__link {
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-body);
  text-decoration: none;
  /* 44px of vertical target from 17px text: the padding is the touch area,
     the border is the affordance. */
  padding-block: 13px;
  border-bottom: 2px solid transparent;
  transition: border-color var(--dur-hover) var(--ease-out),
    color var(--dur-hover) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .masthead__link:hover {
    color: var(--ink);
    border-bottom-color: var(--rule-strong);
  }
}

.masthead__link[aria-current="page"] {
  border-bottom-color: var(--teal-ink);
  color: var(--ink);
}

/* At 390px three links at 17px plus a 40px mark plus gutters is 338px of a
   350px line. It fits, but only just, so the gap tightens rather than the
   type shrinking. Type size is the one thing that does not flex for this
   reader. */

@media (max-width: 429px) {
  .masthead__nav { gap: var(--sp-4); }
}

/* -------------------------------------------------------------------- hero */

/* THE TYPOGRAPHIC FRONT PAGE. Live since 2026-08-31, and it replaced a
   full-bleed video hero that Rob removed: "about the man, i agree. make the
   change and fix it."

   WHAT WAS HERE. About 135 lines styling a five-second clip of a man in his
   twenties, very heavily muscled, doing EZ-bar curls against a cyan studio
   wall -- a three-stop scrim set for the worst frame, a second horizontal
   gradient above 768px, and a media layer for the poster/video swap. It is not
   commented out and it is not left dead in this file. It moved into
   hero-options.html's own <style> block, because that document still needs to
   render treatment A beside treatment B for Rob to see what was chosen against
   what he had, and hero-options.html is not in deploy.sh's FILES. Dead CSS on
   the critical path is paid for by every reader; a decision document's chrome
   is paid for by nobody.

   THE PROBLEM IT SOLVED. The brand spec's reader is 45 to 65, is worried about
   staying capable, and may not train at all. The headline says "for men getting
   back into it". The picture was a competitive physique. Those two things argue
   with each other and the reader settles the argument in under a second, before
   he has read a word. The obvious compromise -- keep the footage, crop away the
   physique -- was checked frame by frame and does not exist: the bar leaves shot
   halfway and the last two seconds are all face.

   WHAT REPLACES IT. Not a different picture. There is no honest one: a
   generated man may not be used, and stock footage of somebody who is not Guy
   is the problem being solved. So the publication's own front page -- the
   promise at display size, and beside it the headlines themselves, which is
   what a serious paper puts above the fold. On a publication whose only asset
   is the writer's judgement, the evidence that it is worth reading is the
   writing.

   WHAT IT COSTS AND WHAT IT SAVES. It gives up the motion Rob originally asked
   for. It takes 1.4MB of video and a 64KB poster off the critical path, and it
   makes the LCP element the H1: text, in a font already preloaded in the head,
   with no network round trip between the reader and the first thing he reads.
   Nothing in this section animates, so there is nothing here for
   prefers-reduced-motion to switch off. */

.herob {
  background: var(--paper);
  border-bottom: 2px solid var(--ink);
}

.herob__in {
  display: grid;
  gap: clamp(var(--sp-7), 6vw, var(--sp-9));
  padding-block: clamp(var(--sp-8), 9vw, var(--sp-10)) clamp(var(--sp-7), 6vw, var(--sp-9));
}

/* The split is 7:5 rather than 1:1. Equal halves read as two things of equal
   importance; the statement leads and the index supports it. */

@media (min-width: 900px) {
  .herob__in {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    align-items: start;
  }
}

.herob__kicker {
  color: var(--teal-ink);
  margin-bottom: var(--sp-5);
}

.herob__head {
  /* THE DISPLAY SCALE, CAPPED, AND THE CAP IS ARITHMETIC.
     --fs-4xl tops out at 84px, which was set for a full-bleed hero where the
     headline had the whole page. Here it sits in the 7fr column of a 7:5 grid,
     which measures 584px at 1440. In Archivo ExtraBold at 84px the words
     "Honest answers" run about 686px, so the first line could not hold two
     words and the headline set with "Honest" alone above three full lines.
     At 68px the same pair measures about 552px and fits, so the headline sets
     three even lines. The cap only engages above roughly 1180px; at 390 and
     768 the clamp is already below it and nothing changes. */
  font-size: min(var(--fs-4xl), 68px);
  font-weight: 800;
  letter-spacing: var(--track-display);
  line-height: var(--lh-tight);
  color: var(--ink);
  max-width: 17ch;

  /* NO text-wrap: balance, and the reasoning got corrected on the way here,
     which is worth keeping rather than tidying away.

     The property used to be here as "a progressive nicety -- nothing depends on
     it". The rendered page showed the headline setting "Honest" alone above
     three full lines, and balance was the obvious suspect. It was not the
     cause: removing it changed nothing, because the first line could not hold
     two words at any wrap strategy. The cause was font size against column
     width, and it is fixed above. Two changes were made on a diagnosis that a
     screenshot then refuted, and only the screenshot could refute it.

     It stays off regardless, on the smaller point the investigation did settle:
     `balance` and `pretty` are both engine-dependent, so a composition that
     leans on either differs between the browser the designer used and the one
     the reader has. Greedy wrapping fills each line and is identical
     everywhere, which is what a masthead statement should be. With three even
     lines there is nothing left for a balancer to improve. */
}

.herob__sub {
  margin-top: var(--sp-5);
  max-width: 44ch;
  font-size: var(--fs-lg);
  line-height: 1.5;
  color: var(--ink-body);
}

.herob__acts {
  margin-top: var(--sp-7);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-5);
}

/* ---- the index, which is the hero's visual ------------------------------
   Three headlines with hairlines. This is deliberately NOT a card grid and
   deliberately carries no images: the point of the treatment is that the
   writing is the evidence, and a row of thumbnails would put the pictures
   back in front of the words. */

.herob__index {
  border-top: 2px solid var(--ink);
  padding-top: var(--sp-4);
}

/* SENTENCE CASE, NOT A TRACKED SMALL-CAPS LABEL, AND THE FINISH GATE IS WHY.
   DESIGN-CONTRACT.md section 4 bans eyebrow inflation: at most one small-caps
   label per three sections, because every section having one is the same
   defect as every section having none. When the typographic hero went live it
   put TWO in a single section -- the kicker "A publication, not a shop" and
   this one -- which is the defect at its most concentrated.

   The kicker stays: it is the positioning line and it is the only place the
   page says outright what it is. This one changes case instead of being
   deleted, because the information is load-bearing -- without it the list
   reads as three published articles rather than as a plan. A sentence-case
   standfirst under a 2px rule is an ordinary newspaper convention and it gives
   the index column its own voice rather than a second copy of the kicker's. */

.herob__indexhead {
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0;
  color: var(--ink-soft);
  margin: 0 0 var(--sp-4);
}

.herob__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.herob__list li + li {
  border-top: 1px solid var(--rule);
}

.herob__item {
  display: block;
  padding-block: var(--sp-4);
  text-decoration: none;
  color: inherit;
}

/* RAISED FROM 17px TO --fs-md WHEN THIS BECAME THE LIVE HERO, 2026-08-31.
   At 17px it was inside the tolerance for a supporting list beside a video.
   As the hero itself, these three headlines are the primary thing a reader is
   meant to read and click, and the contract's 18px floor covers "everywhere a
   reader reads sentences" -- a headline he is deciding whether to open is not
   an exception to that, it is the case it was written for. */

.herob__itemtitle {
  font-family: var(--font-ui);
  font-size: var(--fs-md);
  font-weight: 700;
  line-height: var(--lh-head);
  color: var(--ink);
  margin: 0;
}

.herob__itemmeta {
  display: block;
  margin-top: var(--sp-2);
  font-family: var(--font-ui);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

@media (hover: hover) and (pointer: fine) {
  .herob__item:hover .herob__itemtitle {
    text-decoration: underline;
    text-decoration-thickness: 2px;
    text-underline-offset: 4px;
    text-decoration-color: var(--teal-ink);
  }
}

.herob__item:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* ------------------------------------------------------------------ actions */

/* One button shape. Square-ish, not a pill: a pill on an editorial page reads
   as commerce. 48px minimum height for a thumb and for a hand with tremor. */

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 48px;
  padding: var(--sp-3) var(--sp-5);
  border: 2px solid var(--ink);
  border-radius: 2px;
  background: transparent;
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-hover) var(--ease-out),
    color var(--dur-hover) var(--ease-out),
    border-color var(--dur-hover) var(--ease-out);
}

.btn--solid {
  background: var(--ink);
  color: var(--paper);
}

.btn--light {
  border-color: var(--paper);
  color: var(--paper);
}

/* The one primary action on a dark ground. Solid paper, ink label: 18.98:1,
   and unambiguously the thing to press.

   It exists because the hero previously had two outlined buttons of unequal
   width sitting side by side with identical weight. Two equal calls to action
   is the same as none -- the reader has to choose before they have any basis
   to choose -- and two ghost outlines of different lengths is the specific
   detail that reads as unfinished rather than restrained. */

.btn--invert {
  background: var(--paper);
  border-color: var(--paper);
  color: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
  .btn--invert:hover { background: #ffffff; border-color: #ffffff; }
  .btn--light:hover { background: rgba(255, 255, 255, 0.12); }
  .btn--solid:hover { background: var(--ink-body); }
  .btn:hover .btn__arrow { transform: translateX(3px); }
}

.btn:active {
  transform: translateY(1px);
  transition-duration: var(--dur-press);
}

.btn__arrow {
  transition: transform var(--dur-hover) var(--ease-out);
}

/* The quiet second action. Not a button: a text link with the same target
   height, so the pair reads as one decision with a way past it rather than as
   two competing offers. */

.act-quiet {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--paper);
  /* The underline is a text decoration rather than a bottom border, because a
     border on a 48px-tall flex box draws at the bottom of the BOX and floats
     below the word, detached, at a width that has nothing to do with the text.
     text-decoration hugs the glyphs. The 48px stays as touch target. */
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 6px;
  text-decoration-color: rgba(255, 255, 255, 0.45);
  transition: text-decoration-color var(--dur-hover) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .act-quiet:hover { text-decoration-color: var(--paper); }
}

/* The same action on a paper ground. Ink instead of paper, same shape.

   IT LIVES HERE, NEXT TO WHAT IT MODIFIES, AND THAT IS NOT TIDINESS.
   It used to sit inside the hero's own section further down the file. When the
   typographic hero was promoted and its block moved up above this one on
   2026-08-31, `.act-quiet` and `.act-quiet--ink` were suddenly in the wrong
   order: two single-class selectors of equal specificity, so the later one
   wins, and the base rule's `color: var(--paper)` started painting the
   modifier's text white on a white ground. The link disappeared. Nothing
   errored, no test failed, and it was found by looking at a screenshot.

   A modifier that lives away from its base is a rule whose correctness depends
   on the order of two blocks nobody thinks of as related. Keeping the pair
   adjacent is what makes that impossible rather than merely unlikely. */

.act-quiet--ink {
  color: var(--ink);
  text-decoration-color: var(--rule-strong);
}

@media (hover: hover) and (pointer: fine) {
  .act-quiet--ink:hover { text-decoration-color: var(--ink); }
}

/* A text link inside prose. Underline is an offset border so the descenders of
   g, y and p are not struck through — this reader's eye is doing enough work. */

.link,
.prose p a,
.prose li a {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 2px solid var(--teal);
  transition: background-color var(--dur-hover) var(--ease-out),
    border-color var(--dur-hover) var(--ease-out);
}

/* ------------------------------------------------------------- article list */

/* The homepage's job is to be a table of contents. A list, not a grid of
   cards: a 45-65 reader scans a list by headline and skips a card grid by
   image. Each row is one whole link target, 100% of the row width. */

.pieces {
  list-style: none;
  padding: 0;
  margin: 0;
  border-top: 1px solid var(--rule);
}

.piece {
  border-bottom: 1px solid var(--rule);
}

.piece__link {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-3);
  padding-block: var(--sp-6);
  text-decoration: none;
  color: inherit;
  transition: background-color var(--dur-hover) var(--ease-out);
}

.piece__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-4);
}

.piece__title {
  font-size: var(--fs-xl);
  font-weight: 700;
  max-width: 26ch;
}

.piece__stand {
  max-width: 58ch;
  color: var(--ink-body);
  font-size: var(--fs-md);
}

.piece__more {
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--teal-ink);
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}

.piece__more span {
  display: inline-block;
  transition: transform var(--dur-hover) var(--ease-out);
}

@media (min-width: 768px) {
  .piece__link {
    grid-template-columns: 13rem 1fr;
    gap: var(--sp-6);
    align-items: start;
  }

  .piece__meta {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-2);
    padding-top: var(--sp-1);
  }
}

/* ==========================================================================
   MEDIA CARD — an editorial card whose image swaps to the thing in use
   --------------------------------------------------------------------------
   Reference: the Gymshark collection card, whose image swaps to a second shot
   on hover. The interaction is good and it is worth having. Three things about
   the reference do NOT come across, and they are the reasons this is a rewrite
   rather than a copy:

   1. IT IS NOT A PRODUCT CARD. Guy Flexor owns nothing, stocks nothing, sets
      no price and honours no return. So this card carries a category, a
      headline and a standfirst, and it leads to a REVIEW. It has no price, no
      compare-at, no swatches, no badge, no basket, no stock state and no buy
      affordance of any kind. If this component ever grows one, the storefront
      that was deleted from this domain on 2026-08-31 has come back wearing a
      different coat.

   2. HOVER IS NOT AN INTERACTION MOST OF THIS AUDIENCE HAS. A hover-only swap
      does not exist for a reader on a phone, and this reader is mostly on a
      phone. So the swap is driven by a REAL BUTTON, present on every device,
      keyboard reachable, 48px tall, with a label that says what it does and an
      aria-pressed that says which state it is in. Hover is an accelerator laid
      on top for pointer devices — never the mechanism.

   3. TWO IMAGES PER CARD IS A REAL COST. The second image carries no src at
      all until the reader has shown intent; main.js attaches it. Both carry
      explicit dimensions and the frame owns the aspect ratio, so the swap and
      the load are both shift-free.

   Provenance: the second image is a claim that a person actually used this
   thing, so it can only be Guy's own photograph. test/imagery-check.js
   enforces that, and refuses generated imagery inside a card frame outright.
   ========================================================================== */

/* THE GRID. A lead card and two followers, never three of the same size.

   Three equal cards in a row is the composition every shop uses for a product
   shelf, and on a page that must not read as a shop that is the wrong shape
   before a single word is read. It is also the second place this exact layout
   appeared on one page, which DESIGN-CONTRACT.md §3 forbids outright.

   The asymmetry is not styling: these three reviews are not equally weighted.
   One of them is the piece the section exists to lead with, and a grid that
   says otherwise is making an editorial claim by accident.

   Below 600px every card is the same stacked shape and only the source order
   says which leads. On a phone there is one column, so hierarchy is position,
   and inventing a second card shape to signal it would just make the first
   card taller before the reader has any reason to care. */

.mediacards {
  display: grid;
  gap: var(--sp-7);
  grid-template-columns: 1fr;
  list-style: none;
  padding: 0;
  margin: 0;
}

/* The base card sits BEFORE the modifier deliberately.

   `.mediacard` and `.mediacard--lead` are both single class selectors, so they
   have identical specificity and a media query adds none. Source order is the
   only tie-break. With the base rule written after the modifier, its
   `display: flex` silently beat the modifier's `display: grid` and the lead
   card rendered as a flex column with a frame 3 pixels wide -- which looked,
   in a screenshot, exactly like an image that had failed to load.

   BEM's `--modifier` naming reads like a specialisation of the block and
   invites the assumption that it wins. It carries no such weight. Base rules
   go first. */

.mediacard {
  position: relative;
  display: flex;
  flex-direction: column;
}

/* From 600px the lead becomes a full-width horizontal card -- photograph on
   one side, headline on the other -- and the two followers sit beneath it as a
   pair. Lead story above, secondaries below, which is how a front page has
   always been set.

   The first attempt at this made the lead 1.35fr of a three-column row with a
   4:3 frame while the followers kept 4:5. At those widths the lead's frame
   came out SHORTER than the followers' and it read as the least important card
   in the row. Weight on a page is area and position, not track width, and a
   wider column with a shallower crop is a smaller card. */

@media (min-width: 600px) {
  .mediacards {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(var(--sp-7), 4vw, var(--sp-8)) clamp(var(--sp-5), 3vw, var(--sp-6));
    align-items: start;
  }

  .mediacard--lead {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    column-gap: clamp(var(--sp-6), 4vw, var(--sp-8));
    align-items: center;
    padding-bottom: clamp(var(--sp-6), 4vw, var(--sp-7));
    border-bottom: 1px solid var(--rule);
  }

  /* The lead's frame is landscape because it is beside its own text rather
     than above it, so a portrait crop would set the row height to something
     taller than the words it sits next to. */
  .mediacard--lead .mediacard__frame { aspect-ratio: 3 / 2; }

  .mediacard--lead .mediacard__body { padding-top: 0; }

  /* The followers go square from here up. 4:5 is the right crop on a phone,
     where a single full-width portrait gives an object on a table real
     presence. In a two-column desktop row the same ratio makes each follower
     660px tall against a lead card 401px tall, so the two secondary pieces
     become the largest things in the section and the hierarchy inverts for the
     second time. An aspect ratio chosen for one column is not a decision about
     two. */
  .mediacard:not(.mediacard--lead) .mediacard__frame { aspect-ratio: 1 / 1; }
}

/* The frame owns the aspect ratio, so the grid's row heights are settled
   before a single byte of image arrives. 4:5 portrait for the followers: it
   gives an object on a table real presence beside a headline, where a
   landscape crop would be a strip. The lead card takes 4:3, because at its
   width a portrait frame would be a column of photograph taller than the
   viewport. */

.mediacard__frame {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--paper-sunk);
  border: 1px solid var(--rule);
}

/* The lead card's own type steps up with it. One extra step, not a whole
   second scale: the point of the asymmetry is which piece leads, and a
   separate type system for the followers would make it read as two different
   components rather than one component at two weights.

   Below 600px the lead is a stacked card like the others, so it keeps the
   default sizes and only the source order says it is first. */

@media (min-width: 600px) {
  .mediacard--lead .mediacard__title { font-size: var(--fs-xl); }
  .mediacard--lead .mediacard__stand { font-size: var(--fs-md); }
}

.mediacard__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* opacity AND visibility, not opacity alone. Visibility takes the hidden
   image out of the accessibility tree, so exactly one alt text is exposed at
   a time; delaying the visibility change until the fade finishes keeps the
   transition smooth. Two different photographs of two different things should
   not both be announced. */

.mediacard__img--b {
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--dur-ui) var(--ease-out),
    visibility 0s linear var(--dur-ui);
}

.mediacard__img--a {
  opacity: 1;
  visibility: visible;
  transition:
    opacity var(--dur-ui) var(--ease-out),
    visibility 0s linear 0s;
}

.mediacard.is-swapped .mediacard__img--b {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

.mediacard.is-swapped .mediacard__img--a {
  opacity: 0;
  visibility: hidden;
  transition-delay: 0s, var(--dur-ui);
}

/* The swap control. A real button, not a hover target. 48px tall, high
   contrast against an unknown photograph behind it, and it says what it does
   rather than showing a glyph nobody has to decode. */

.mediacard__swap {
  position: absolute;
  z-index: 2;
  left: var(--sp-3);
  bottom: var(--sp-3);
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 48px;
  padding: 0 var(--sp-4);
  border: 2px solid var(--ink);
  border-radius: 2px;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-weight: 700;
  cursor: pointer;
  transition:
    background-color var(--dur-hover) var(--ease-out),
    color var(--dur-hover) var(--ease-out);
}

/* The pressed state has to LOOK pressed. A toggle that renders identically in
   both states tells a reader nothing about which one he is in, and the label
   change alone is easy to miss on a small screen. This is not a hover style —
   it is inverted whenever aria-pressed is true, on every device. */

.mediacard__swap[aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper);
}

.mediacard__swapicon {
  font-size: 1.1em;
  line-height: 1;
}

/* The card body. Category, headline, standfirst. Nothing else is permitted
   here — see note 1 at the top of this block. */

.mediacard__body {
  padding-top: var(--sp-4);
  display: grid;
  gap: var(--sp-2);
}

/* No max-width: the grid column is already the measure, and capping it as
   well forced "push-up" to break across a line in a column with room to
   spare. Two constraints on the same axis is one too many. */

.mediacard__title {
  font-size: var(--fs-sm);
  font-weight: 700;
}

.mediacard__stand {
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--ink-soft);
  max-width: 40ch;
}

/* The whole card is the link target, via a stretched pseudo-element on the
   title's anchor. The swap button sits above it on z-index 2, which is the
   standard way to put a second action inside a card without nesting
   interactive elements — which is invalid HTML and breaks keyboard order. */

.mediacard__link {
  color: inherit;
  text-decoration: none;
}

.mediacard__link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

.mediacard__link:focus-visible {
  outline: none;
}

.mediacard:has(.mediacard__link:focus-visible) .mediacard__frame {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  box-shadow: 0 0 0 6px var(--focus-halo);
}

/* An inline single card, for use inside an article. Horizontal above 620px so
   it does not interrupt the read with a full-width portrait. */

.mediacard--inline {
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding-block: var(--sp-6);
}

@media (min-width: 620px) {
  .mediacard--inline {
    display: grid;
    grid-template-columns: 13rem minmax(0, 1fr);
    gap: var(--sp-6);
    align-items: start;
  }

  .mediacard--inline .mediacard__frame {
    aspect-ratio: 1 / 1;
  }

  .mediacard--inline .mediacard__body {
    padding-top: 0;
  }

  .mediacard--inline .mediacard__title {
    font-size: var(--fs-lg);
  }

  .mediacard--inline .mediacard__stand {
    font-size: var(--fs-md);
    color: var(--ink-body);
  }
}

/* ---------------------------------------------------------------- the person */

/* "Show that a real person with a real history is behind it." The portrait is
   a reserved box with an explicit aspect ratio so it costs no CLS whether or
   not a real photograph ever lands in it. */

.who {
  display: grid;
  gap: var(--sp-7);
  align-items: start;
}

@media (min-width: 768px) {
  .who {
    grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
    gap: clamp(var(--sp-7), 6vw, var(--sp-9));
  }
}

.who__portrait {
  margin: 0;
  aspect-ratio: 4 / 5;
  background: var(--paper-sunk);
  border: 1px solid var(--rule);
  display: grid;
  place-items: center;
  padding: var(--sp-5);
  text-align: center;
}

.who__portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.who__body p {
  font-size: var(--fs-article);
  line-height: var(--lh-read);
  max-width: 54ch;
}

.who__body p + p {
  margin-top: var(--sp-5);
}

.who__sign {
  margin-top: var(--sp-6);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  color: var(--ink-soft);
}

/* ------------------------------------------------------------- placeholder */

/* Anything that is standing in for content nobody has written yet wears this.
   It is deliberately not subtle. The failure this site is least able to
   survive is an invented product claim or an invented personal experience
   shipping because it looked finished. */

.ph {
  border: 1px dashed var(--rule-strong);
  background: repeating-linear-gradient(
    135deg,
    transparent,
    transparent 9px,
    rgba(184, 178, 167, 0.14) 9px,
    rgba(184, 178, 167, 0.14) 18px
  );
  padding: var(--sp-4) var(--sp-5);
  color: var(--ink-soft);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  line-height: 1.5;
}

.ph__tag {
  display: inline-block;
  font-size: var(--fs-3xs);
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--caution);
  margin-bottom: var(--sp-2);
}

.ph--inline {
  display: inline;
  padding: 2px 6px;
  border-style: dotted;
}

/* A standalone note explaining what a whole block of placeholder is. */

.ph--block {
  max-width: var(--measure-wide);
  margin-bottom: var(--sp-7);
}

/* Inside a box that already carries its own border and ground. */

.ph--bare {
  border: 0;
  background: none;
  padding: 0;
}

.ph p {
  margin: 0;
}

.ph p + p {
  margin-top: var(--sp-3);
}

/* ------------------------------------------------------------ how I work */

/* Three editorial policies. NOT a shop promise strip: every line is something
   Guy himself controls and can be held to. Nothing about shipping, returns,
   warranty or price appears here or anywhere else in persistent chrome,
   because persistent chrome is a contractual promise and Guy owns none of
   those terms -- they belong to a seller he does not control.

   THIS IS THE MOST IMPORTANT CONTENT ON THE HOMEPAGE AND IT USED TO LOOK LIKE
   THE LEAST. It was three equal columns of 18px text under a teal hairline,
   which is the standard feature-strip composition, which is to say it read as
   decoration a reader skips. On an affiliate publication the only thing being
   sold is Guy's judgement, so these three sentences ARE the product. Rendering
   the product at the smallest size on the page is a hierarchy inversion, and
   no amount of correct spacing fixes it.

   So it is now a numbered editorial stack at full width: a numeral, a heading
   a reader takes in at a glance, and the sentence at reading size with real
   air around it. It is deliberately NOT three columns, both because
   three-equal-columns is banned by DESIGN-CONTRACT.md §3 and because these are
   sequential commitments rather than parallel features. The first one is the
   one that matters most, and a row of three says they are interchangeable.

   The numeral is generated in CSS, so it never reaches the accessibility tree
   and a screen reader hears three commitments rather than "01 you know before
   you click". */

.policy {
  list-style: none;
  padding: 0;
  margin: 0;
  counter-reset: policy;
  border-top: 1px solid var(--rule);
  font-size: var(--fs-md);
  line-height: var(--lh-read);
  color: var(--ink-body);
}

/* Every item is placed EXPLICITLY. An auto-placed grid puts each child in the
   next free cell, which is fine until the number of children stops matching
   the number of columns -- and then a track sizes itself to content nobody
   intended to put there. Naming the column each part belongs to costs three
   declarations and removes the whole class of failure. */

.policy li {
  counter-increment: policy;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--sp-5);
  align-items: start;
  padding-block: clamp(var(--sp-6), 4vw, var(--sp-8));
  border-bottom: 1px solid var(--rule);
}

.policy li::before { grid-column: 1; grid-row: 1; }
.policy strong { grid-column: 2; grid-row: 1; }
.policy p { grid-column: 2; grid-row: 2; margin: 0; }

.policy li::before {
  content: counter(policy, decimal-leading-zero);
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  color: var(--teal-ink);
  min-width: 2.5ch;
  /* Optically seated against the cap height of the heading beside it. Without
     this the numeral floats a couple of pixels high, because a 17px line box
     and a 28px one do not share a top edge. */
  padding-top: 0.42em;
}

.policy strong {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--fs-xl);
  font-weight: 700;
  line-height: var(--lh-head);
  letter-spacing: var(--track-display);
  color: var(--ink);
  max-width: 24ch;
  margin-bottom: var(--sp-3);
}

/* Wide enough for a hanging heading column: the three headings then line up as
   a list a reader scans in one pass, and the three sentences sit in a single
   reading measure beside them. Baseline alignment does the seating here, so
   the numeral's optical nudge is not wanted and is removed. */

@media (min-width: 900px) {
  .policy li {
    grid-template-columns: auto minmax(0, 19rem) minmax(0, 1fr);
    column-gap: var(--sp-7);
    align-items: baseline;
  }

  .policy li::before { padding-top: 0; }
  .policy p { grid-column: 3; grid-row: 1; }

  .policy strong {
    margin-bottom: 0;
    max-width: none;
  }
}

/* ------------------------------------------------------------- mailing list */

/* The list has a HOME here, and deliberately no live form.
   sql/SETUP.md lists six steps that only Rob can do, and api/ is not in
   deploy.sh's FILES array, so a form posted today reaches a 404. A styled
   input whose handler does nothing spends the reader's one moment of intent
   and returns nothing — DEMAND-CAPTURE-PLAN.md §3 is explicit about this, and
   a visitor who never saw a box is in a better state than one who used it and
   was discarded. The markup for the real form is in signup-snippet.html and
   drops straight into .list__slot when the backend is live. */

.list__grid {
  display: grid;
  gap: var(--sp-7);
  align-items: start;
}

@media (min-width: 768px) {
  .list__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 26rem);
    gap: clamp(var(--sp-7), 6vw, var(--sp-9));
  }
}

.list__body p {
  font-size: var(--fs-article);
  line-height: var(--lh-read);
  max-width: 48ch;
}

.list__body p + p {
  margin-top: var(--sp-4);
}

.list__slot {
  border: 1px solid var(--rule-strong);
  background: var(--paper);
  padding: var(--sp-6);
}

.section--ink .list__slot {
  background: #141413;
  border-color: var(--rule-strong);
}

.list__slot h3 {
  font-size: var(--fs-sm);
  font-weight: 700;
  margin-bottom: var(--sp-3);
}

.list__slot p {
  font-size: var(--fs-xs);
  color: var(--ink-soft);
}

/* The real form, styled and ready, for when .list__slot gets filled. */

.field {
  width: 100%;
  min-height: 50px;
  padding: var(--sp-3) var(--sp-4);
  border: 2px solid var(--rule-strong);
  border-radius: 2px;
  background: var(--paper);
  color: var(--ink-body);
  font-family: var(--font-ui);
  font-size: var(--fs-md);
  transition: border-color var(--dur-hover) var(--ease-out);
}

.field:focus {
  border-color: var(--ink);
}

.signup__hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.signup__msg {
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-xs);
  min-height: 1.4em;
}

.signup__msg[data-state="error"] {
  color: var(--caution);
}

/* ------------------------------------------------------------------ footer */

.foot {
  background: var(--ink);
  color: #c9c6c0;
  --focus: var(--paper);
  padding-block: var(--band) var(--sp-7);
}

.foot__grid {
  display: grid;
  gap: var(--sp-7);
  grid-template-columns: 1fr;
}

@media (min-width: 768px) {
  .foot__grid {
    grid-template-columns: minmax(0, 1.4fr) repeat(2, minmax(0, 1fr));
    gap: var(--sp-8);
  }
}

.foot h4 {
  font-size: var(--fs-3xs);
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--teal);                            /* 10.15:1 on ink */
  margin-bottom: var(--sp-4);
}

.foot ul {
  list-style: none;
  padding: 0;
  display: grid;
  gap: var(--sp-3);
}

.foot a {
  color: #c9c6c0;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  font-size: var(--fs-xs);
  transition: color var(--dur-hover) var(--ease-out),
    border-color var(--dur-hover) var(--ease-out);
}

/* The wordmark's text fallback is near-black by default; on the footer's ink
   ground that renders it as an invisible word with a teal full stop. */

.foot .mark__text {
  color: var(--paper);
}

/* Placeholder chips on the dark ground. The light stripe pattern and the
   --ink-soft text are both invisible here, and an unreadable "not written yet"
   marker is worse than none: it looks like a rendering fault rather than a
   deliberate gap. */

.foot .ph {
  border-color: #46443f;
  color: #c9c6c0;
  background: repeating-linear-gradient(
    135deg,
    transparent,
    transparent 9px,
    rgba(201, 198, 192, 0.1) 9px,
    rgba(201, 198, 192, 0.1) 18px
  );
}

.foot__blurb {
  font-size: var(--fs-xs);
  color: #a8a49d;                                /* 7.98:1 on ink */
  max-width: 40ch;
  margin-top: var(--sp-4);
}

.foot__base {
  margin-top: var(--sp-8);
  padding-top: var(--sp-5);
  border-top: 1px solid #2b2a28;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-6);
  justify-content: space-between;
  font-size: var(--fs-2xs);
  color: #a8a49d;
}

/* ==========================================================================
   ARTICLE TEMPLATE
   --------------------------------------------------------------------------
   This is the page that earns the money, so it is the page that matters most.
   Everything here answers one question: can a man with reading glasses get
   through 1,800 words of this on a phone, one-handed, and know at every point
   what kind of thing he is reading?

   The structural blocks below are the review skeleton from
   DEMAND-CAPTURE-PLAN.md §3, in order: disclosure, what it is, what I did,
   what is wrong with it, who should skip, the honest alternative, price as a
   dated fact, link, specs, link.
   ========================================================================== */

.piecehead {
  padding-block: var(--band) var(--sp-7);
  border-bottom: 1px solid var(--rule);
}

.piecehead__kicker {
  margin-bottom: var(--sp-5);
}

.piecehead h1 {
  font-size: var(--fs-3xl);
  font-weight: 800;
  letter-spacing: var(--track-display);
  max-width: 20ch;
}

.piecehead__stand {
  margin-top: var(--sp-5);
  max-width: 52ch;
  font-size: var(--fs-lg);
  line-height: 1.5;
  color: var(--ink-body);
}

.byline {
  margin-top: var(--sp-6);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-5);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  color: var(--ink-soft);
}

.byline strong {
  color: var(--ink);
  font-weight: 700;
}

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

/* ---- in-page contents ---------------------------------------------------
   Free, no JavaScript, and genuinely useful: this template runs to about
   fourteen phone screens. It also lets a reader arriving cold see that the
   skip section exists before deciding whether to trust the page. */

.toc {
  margin-block: var(--sp-7);
  padding: var(--sp-5) var(--sp-6);
  border: 1px solid var(--rule);
  background: var(--paper);
  max-width: var(--measure);
}

.toc ol {
  list-style: none;
  padding: 0;
  margin-top: var(--sp-4);
  display: grid;
  gap: var(--sp-3);
  counter-reset: toc;
}

.toc li {
  counter-increment: toc;
  font-size: var(--fs-sm);
  line-height: 1.4;
}

.toc li::before {
  content: counter(toc) ".";
  font-family: var(--font-ui);
  font-weight: 700;
  color: var(--ink-soft);
  margin-right: var(--sp-3);
}

.toc a {
  color: var(--ink-body);
  text-decoration: none;
  border-bottom: 1px solid var(--rule-strong);
}

@media (hover: hover) and (pointer: fine) {
  .toc a:hover {
    border-bottom-color: var(--teal-ink);
    color: var(--ink);
  }
}

/* A media-shaped placeholder that holds its own aspect ratio, so swapping a
   real image in changes nothing about the layout. */

.ph--media {
  aspect-ratio: 16 / 10;
  display: grid;
  place-items: center;
  text-align: center;
}

.ph--block-top {
  margin-top: var(--sp-6);
}

/* ==========================================================================
   THE ARTICLE INDEX
   --------------------------------------------------------------------------
   Reference: the Gymshark blog index — a featured piece at large size, then
   compact cards with thumbnail, category and date.

   THE ACTUAL DESIGN PROBLEM, and it is the whole reason this looks the way it
   does: Guy launches with TWO articles. Almost every blog template is drawn
   at twenty and looks broken at two — a hero, then a three-column grid
   holding one lonely card, then a Load More that loads nothing. Emptiness
   reads as abandonment, and on a publication whose only asset is trust that
   is an expensive first impression.

   So the index degrades upward rather than downward:

     1 piece    the lead, then what is coming
     2 pieces   the lead, then ONE secondary at half the lead's weight, then
                what is coming. Two pieces given two different weights reads
                as an editor's judgement. Two identical cards side by side
                reads as a grid that ran out.
     3-6        the lead, then the secondaries as a grid
     7+         as above, plus Load More

   Load More is NOT in this markup, deliberately. A button that reveals
   nothing is a claim about the size of the archive that the archive does not
   support, and it is the same class of mistake as a signup form with no
   backend: it spends the reader's action and returns nothing. Add it when
   there is a seventh piece, not before. The markup says so at the point where
   it would go.

   "What's coming" is what fills the page instead, and it is filled with
   something TRUE — the real publishing plan — rather than with fake cards.
   ========================================================================== */

.lead {
  display: grid;
  gap: var(--sp-5);
}

@media (min-width: 860px) {
  .lead {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    gap: clamp(var(--sp-6), 4vw, var(--sp-8));
    align-items: center;
  }
}

.lead__frame {
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background: var(--paper-sunk);
  border: 1px solid var(--rule);
}

.lead__frame img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.lead__body {
  display: grid;
  gap: var(--sp-4);
}

/* A headline is not a body link and must not be underlined like one: at
   display size a 2px rule under every descender reads as damage. The hover
   and focus states carry the affordance instead. */

.lead__title a,
.second__title a {
  border-bottom: 0;
  text-decoration: none;
}

@media (hover: hover) and (pointer: fine) {
  .lead__title a:hover,
  .second__title a:hover {
    background: none;
    text-decoration: underline;
    text-decoration-color: var(--teal);
    text-decoration-thickness: 3px;
    text-underline-offset: 4px;
  }
}

.lead__title {
  font-size: var(--fs-2xl);
  font-weight: 800;
  letter-spacing: var(--track-display);
  max-width: 18ch;
}

.lead__stand {
  font-size: var(--fs-lg);
  line-height: 1.45;
  max-width: 44ch;
}

/* The secondary piece. Deliberately NOT the same weight as the lead — that
   difference is what makes two articles read as an edited page rather than a
   grid with a gap in it. */

.second {
  display: grid;
  gap: var(--sp-5);
  padding-top: var(--sp-7);
  margin-top: var(--sp-8);
  border-top: 1px solid var(--rule);
}

@media (min-width: 620px) {
  .second {
    grid-template-columns: 15rem minmax(0, 1fr);
    gap: var(--sp-6);
    align-items: start;
  }
}

.second__frame {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--paper-sunk);
  border: 1px solid var(--rule);
}

.second__frame img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.second__title {
  margin-top: var(--sp-3);
  font-size: var(--fs-xl);
  font-weight: 700;
  max-width: 26ch;
}

.second__by {
  margin-top: var(--sp-3);
}

.second__stand {
  margin-top: var(--sp-3);
  font-size: var(--fs-md);
  color: var(--ink-body);
  max-width: 54ch;
}

/* Category tag and date, together. Both are adopted from the reference and
   the date is not optional: a health publication that hides when something
   was written reads as evasive, and "is this still current" is exactly the
   question this reader is asking. */

.stamp {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-4);
}

.stamp__tag {
  display: inline-block;
  padding: 3px var(--sp-3);
  border: 1px solid var(--teal-ink);
  border-radius: 2px;
  color: var(--teal-ink);
  font-family: var(--font-ui);
  font-size: var(--fs-3xs);
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.stamp__date {
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  color: var(--ink-soft);
}

/* ---- the byline, and why it is a component ------------------------------
   Guy's only asset is his judgement, and a reader cannot weigh judgement that
   has no name on it. So the byline is not decoration and not a line of grey
   text at 13px: it carries a face, a name that links to a real author page,
   and the one line about what this man does and does not know.

   The portrait is a fixed 56px circle with an explicit box, so it costs no
   layout shift whether or not a photograph ever lands in it. */

.byline {
  margin-top: var(--sp-6);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-5);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  color: var(--ink-soft);
}

.byline strong {
  color: var(--ink);
  font-weight: 700;
}

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

.byline--rich {
  align-items: flex-start;
  gap: var(--sp-4);
  padding-block: var(--sp-5);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.byline__face {
  flex: none;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--paper-sunk);
  border: 1px solid var(--rule-strong);
  display: grid;
  place-items: center;
  font-size: var(--fs-3xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--caution);
  text-align: center;
  line-height: 1.1;
}

.byline__face img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.byline__text {
  display: grid;
  gap: var(--sp-1);
  min-width: 0;
}

.byline__name {
  font-size: var(--fs-sm);
  color: var(--ink);
}

.byline__name a {
  color: inherit;
  border-bottom: 2px solid var(--teal);
  text-decoration: none;
}

.byline__scope {
  font-size: var(--fs-xs);
  color: var(--ink-soft);
  max-width: 52ch;
}

.byline__dates {
  font-size: var(--fs-2xs);
  color: var(--ink-soft);
}

/* ---- sourcing, and the accuracy line ------------------------------------
   The one thing Guy has that a brand's blog structurally cannot: the
   physiology is graded before the sentences are written, and the evidence
   categories are never blended. So this is designed as an ASSET at the foot
   of the article — a bordered block with the reviewer named and dated, and a
   source list where every entry carries its grade — rather than as fine print.

   The grade chips are not decoration. Established evidence, early research,
   expert opinion, community experience and a marketing claim are five
   different kinds of thing, and the whole method depends on a reader being
   able to see which one he is being handed. */

.sourcing {
  margin-top: var(--sp-8);
  border: 2px solid var(--ink);
  background: var(--paper);
}

.sourcing__head {
  padding: var(--sp-5) clamp(var(--sp-5), 4vw, var(--sp-6));
  background: var(--ink);
  color: var(--paper);
}

.sourcing__head h2 {
  color: var(--paper);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.sourcing__checked {
  margin-top: var(--sp-2);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  color: #c9c6c0;
  max-width: 60ch;
}

.sourcing__body {
  padding: clamp(var(--sp-5), 4vw, var(--sp-6));
}

.sourcing ol {
  list-style: none;
  padding: 0;
  display: grid;
  gap: var(--sp-5);
  counter-reset: src;
}

.sourcing li {
  counter-increment: src;
  display: grid;
  gap: var(--sp-2);
  font-size: var(--fs-xs);
  line-height: 1.5;
  padding-left: var(--sp-6);
  position: relative;
}

.sourcing li::before {
  content: counter(src);
  position: absolute;
  left: 0;
  top: 0;
  font-family: var(--font-ui);
  font-weight: 700;
  color: var(--ink-soft);
}

/* Grade chips. Five values, and they are never merged. */

.grade {
  justify-self: start;
  display: inline-block;
  padding: 2px var(--sp-2);
  border-radius: 2px;
  font-family: var(--font-ui);
  font-size: var(--fs-3xs);
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  border: 1px solid currentColor;
}

/* Computed, not estimated — the chip is a border and text in one colour, so
   each has to clear 4.5:1 as text on both grounds it can sit on. Two of these
   were guessed wrong before they were measured, which is exactly why they are
   asserted in test/contrast-check.js rather than trusted here. */

.grade--established { color: #0b5c2e; }   /* 7.79:1 paper · 7.07:1 sunk */
.grade--early       { color: #0b6f6a; }   /* 5.76:1 paper · 5.23:1 sunk */
.grade--opinion     { color: #5a4a12; }   /* 8.31:1 paper · 7.54:1 sunk */
.grade--community   { color: #55524d; }   /* 7.46:1 paper · 6.77:1 sunk */
.grade--marketing   { color: #8a1c10; }   /* 8.93:1 paper · 8.11:1 sunk */

.sourcing__unknown {
  margin-top: var(--sp-6);
  padding-top: var(--sp-5);
  border-top: 2px solid var(--rule-strong);
  font-size: var(--fs-md);
  line-height: var(--lh-read);
  max-width: 56ch;
}

/* ---- the author page ---------------------------------------------------- */

.author {
  display: grid;
  gap: var(--sp-7);
  align-items: start;
}

@media (min-width: 768px) {
  .author {
    grid-template-columns: minmax(0, 18rem) minmax(0, 1fr);
    gap: clamp(var(--sp-7), 6vw, var(--sp-9));
  }
}

.author__portrait {
  margin: 0;
  aspect-ratio: 4 / 5;
  background: var(--paper-sunk);
  border: 1px solid var(--rule);
  display: grid;
  place-items: center;
  padding: var(--sp-5);
}

.author__portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.decision {
  border-left: 4px solid var(--caution);
  background: var(--paper-sunk);
  padding: var(--sp-5) var(--sp-6);
}

.decision + .decision {
  margin-top: var(--sp-5);
}

.decision h3 {
  font-size: var(--fs-sm);
  font-weight: 700;
  margin-bottom: var(--sp-3);
}

.decision p {
  font-size: var(--fs-md);
  line-height: var(--lh-read);
  max-width: 56ch;
}

.decision p + p {
  margin-top: var(--sp-3);
}

.decision__who {
  margin-top: var(--sp-4);
  font-family: var(--font-ui);
  font-size: var(--fs-3xs);
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--caution);
}

/* ---- prose --------------------------------------------------------------
   19-20px, 1.7 leading, 40rem measure. These three numbers are the whole
   reason this template exists; do not tune them to fit a layout. */

.prose {
  font-size: var(--fs-article);
  line-height: var(--lh-read);
  max-width: var(--measure);
}

.prose > * + * {
  margin-top: var(--sp-5);
}

.prose h2 {
  font-size: var(--fs-2xl);
  font-weight: 700;
  margin-top: var(--sp-8);
  max-width: 22ch;
}

.prose h3 {
  font-size: var(--fs-xl);
  font-weight: 700;
  margin-top: var(--sp-7);
  max-width: 26ch;
}

.prose h2 + *,
.prose h3 + * {
  margin-top: var(--sp-4);
}

.prose ul,
.prose ol {
  padding-left: var(--sp-5);
}

.prose li + li {
  margin-top: var(--sp-3);
}

.prose strong {
  font-weight: 700;
  color: var(--ink);
}

/* ---- the article column -------------------------------------------------
   On a 1440px screen a 640px measure pinned to the left edge reads as a page
   that failed to finish loading. So the whole article lives in its own
   centred column, 52rem wide plus gutters, and every block inside it centres:
   prose at 40rem, figures and tables at 52rem. The two left edges do not
   align, which is correct — a symmetric breakout is a deliberate shape,
   whereas a text column parked against the viewport edge is an accident.

   Wide blocks are siblings of .prose, never children of it. A 52rem child
   inside a 40rem parent has to be dragged out with negative margins, and that
   arithmetic breaks the moment the parent is narrower than the measure —
   which on a phone it always is. */

/* A content grid, not a stack of margins. Five tracks: two flexible edges, a
   6rem shoulder on each side, and the reading measure in the middle. Every
   block declares which track it belongs to, so nothing is centred by
   arithmetic and nothing fights the component's own margin rules.

   On a phone the shoulders collapse to 0 and "wide" becomes "text", which is
   the correct behaviour rather than a fallback: at 390px there is no width to
   break out into.

   THAT LAST PARAGRAPH WAS A STATEMENT OF INTENT THAT THE CODE DID NOT
   IMPLEMENT, and it went unnoticed for as long as it did precisely because it
   was written down — a reader of this file checked the comment and stopped.
   Measured on 2026-08-31 at 390px, on both article.html and wrists.html:

       track widths   20 | 96 | 158 | 96 | 20
       text column    158px
       measure        ~17 characters per line

   Seventeen characters is not a narrow measure, it is a column of broken
   words, and it was the state of every article on this site on the primary
   device for the primary audience.

   WHY IT HAPPENED. `minmax(0, 6rem)` has a maximum of 6rem and a minimum of 0,
   but grid does not treat the middle track as more deserving. Free space is
   distributed to all three growable tracks together, so the shoulders reach
   their 96px cap long before the measure is satisfied and the reading column
   gets whatever is left. The shoulders only collapse when there is no free
   space at all, which at 390px there very nearly is.

   THE FIX is mobile-first and explicit rather than emergent: no shoulders by
   default, and shoulders added back at 900px, which is the first width that
   can hold the 640px measure plus two 96px shoulders plus two 20px gutters
   (872px) without taking anything from the text. 900px is already a
   breakpoint in this file; this adds no new step to the scale.

   Below 900px "wide" and "text" resolve to the same track, which is what the
   paragraph above always said should happen. Nothing that sits in the wide
   band needs a separate rule.

   Tripwire, per DESIGN-CONTRACT.md: if the shoulder value, --measure or the
   900px step is ever changed, re-measure the text track at 390px. It must
   stay above roughly 320px. A rendered measurement, not a reading of this
   comment — that is the mistake this block documents. */

.wrap--article {
  display: grid;
  padding-inline: 0;
  max-width: none;
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [wide-start] 0
    [text-start] minmax(0, var(--measure)) [text-end]
    0 [wide-end]
    minmax(var(--gutter), 1fr) [full-end];
}

@media (min-width: 900px) {
  .wrap--article {
    grid-template-columns:
      [full-start] minmax(var(--gutter), 1fr)
      [wide-start] minmax(0, 6rem)
      [text-start] minmax(0, var(--measure)) [text-end]
      minmax(0, 6rem) [wide-end]
      minmax(var(--gutter), 1fr) [full-end];
  }
}

.wrap--article > * {
  grid-column: text-start / text-end;
}

.wrap--article > .disclose,
.wrap--article > .breakout,
.wrap--article > .skip,
.wrap--article > .piecefoot,
.wrap--article > .sourcing,
.wrap--article > .author,
.wrap--article > .section__head,
.wrap--article > .ph--block {
  grid-column: wide-start / wide-end;
}

/* The rule that decides which pages use this grid: a page whose job is
   READING gets the centred article column (article.html, about.html); a page
   whose job is FINDING gets the full page wrap, left-aligned (index.html,
   articles.html). Mixing the two on one page is what makes a site feel like
   several sites. */

.breakout {
  max-width: var(--measure-wide);
  margin-top: var(--sp-7);
  margin-bottom: var(--sp-7);
}

/* ---- pull quote ---------------------------------------------------------
   A pull quote is a navigation aid, not decoration: it is what the reader who
   is skimming reads instead of the paragraph. Serif italic, one size up, with
   a teal rule that carries the brand without ever being text. */

.pullquote {
  margin-top: var(--sp-7);
  margin-bottom: var(--sp-7);
  padding-left: clamp(var(--sp-4), 3vw, var(--sp-6));
  border-left: 3px solid var(--teal);
  font-size: var(--fs-lg);
  font-style: italic;
  line-height: 1.45;
  color: var(--ink);
  max-width: 34ch;
}

.pullquote cite {
  display: block;
  margin-top: var(--sp-3);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-style: normal;
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* ---- figures ------------------------------------------------------------
   width and height are REQUIRED on every <img> inside a figure. CLS budget is
   0.1 and an image without dimensions is the single commonest way to blow it. */

.figure {
  margin: 0;
}

.figure img {
  width: 100%;
  background: var(--paper-sunk);
}

.figure figcaption {
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--rule);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--ink-soft);
  max-width: 52ch;
}

/* ---- comparison table ---------------------------------------------------
   The table scrolls inside its own container, never the page. The container
   is focusable and labelled so a keyboard user can reach the scroll, which is
   the part almost every implementation forgets. */

.tablewrap {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--rule);
  background: var(--paper);
}

.tablewrap:focus-visible {
  outline-offset: -3px;
}

.tablewrap table {
  border-collapse: collapse;
  width: 100%;
  min-width: 34rem;
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
}

.tablewrap caption {
  caption-side: top;
  text-align: left;
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--rule);
  background: var(--paper-sunk);
  font-family: var(--font-ui);
  font-size: var(--fs-3xs);
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-soft);
}

.tablewrap th,
.tablewrap td {
  padding: var(--sp-4) var(--sp-5);
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--rule);
  line-height: 1.5;
}

.tablewrap thead th {
  background: var(--paper-sunk);
  color: var(--ink);
  font-weight: 700;
  white-space: nowrap;
}

.tablewrap tbody th {
  color: var(--ink);
  font-weight: 700;
  min-width: 11rem;
}

.tablewrap tbody tr:last-child th,
.tablewrap tbody tr:last-child td {
  border-bottom: 0;
}

.tablewrap__hint {
  margin-top: var(--sp-3);
  font-family: var(--font-ui);
  font-size: var(--fs-2xs);
  color: var(--ink-soft);
}

/* ---- "who should skip this" --------------------------------------------
   Cole's finding: on an affiliate product the who-should-skip section IS the
   risk reversal, because the guarantee belongs to someone else. A guarantee
   says "if you are wrong you get your money back"; this says "here is how to
   know you would be wrong before you spend it". That makes it a structural
   part of the page, not an aside — so it gets the strongest separation on the
   template: a heavy top rule, its own ground, and its own label.

   It shares a visual family with .disclose (sunk ground, thick left border)
   because both are blocks that qualify what you are reading. The border
   colour is what tells them apart: teal for the disclosure, ink for this. */

.skip {
  margin-top: var(--sp-8);
  margin-bottom: var(--sp-8);
  border-top: 4px solid var(--ink);
  border-left: 3px solid var(--ink);
  background: var(--paper-sunk);
  padding: var(--sp-6) clamp(var(--sp-5), 4vw, var(--sp-7));
  max-width: var(--measure-wide);
}

.skip__label {
  color: var(--caution);                         /* 8.93:1 on paper */
  margin-bottom: var(--sp-3);
}

.skip h2 {
  font-size: var(--fs-xl);
  font-weight: 700;
  margin: 0 0 var(--sp-4);
  max-width: 24ch;
}

.skip ul {
  list-style: none;
  padding: 0;
  display: grid;
  gap: var(--sp-4);
  font-size: var(--fs-md);
  line-height: var(--lh-read);
  max-width: 56ch;
}

.skip li {
  padding-left: var(--sp-6);
  position: relative;
}

/* An em dash, not a tick or a cross. A cross reads as an error state and a
   tick reads as a feature list; this is neither. */

.skip li::before {
  content: "\2014";
  position: absolute;
  left: 0;
  top: 0;
  color: var(--caution);
  font-weight: 700;
}

.skip__foot {
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--rule-strong);
  font-size: var(--fs-xs);
  color: var(--ink-soft);
  max-width: 56ch;
}

/* ---- the honest alternative, and price as a dated fact ------------------
   Guy does not set prices and cannot promise terms. A price is reported the
   way a fact is reported: with a date and an owner. This block exists so the
   layout makes that unavoidable rather than optional. */

.stated {
  margin-top: var(--sp-7);
  margin-bottom: var(--sp-7);
  border-left: 3px solid var(--rule-strong);
  padding-left: clamp(var(--sp-4), 3vw, var(--sp-6));
  max-width: 52ch;
}

.stated__label {
  margin-bottom: var(--sp-3);
}

.stated p {
  font-size: var(--fs-md);
  line-height: var(--lh-read);
}

.stated p + p {
  margin-top: var(--sp-3);
}

.stated__when {
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  color: var(--ink-soft);
}

/* ---- the outbound link --------------------------------------------------
   Names the destination and the action, never the terms. Not a pill, not
   green, not "Buy now" — see DEMAND-CAPTURE-PLAN.md §3. */

.outbound {
  margin-top: var(--sp-5);
  margin-bottom: var(--sp-7);
}

.outbound .btn {
  max-width: 100%;
}

.outbound__note {
  margin-top: var(--sp-3);
  font-family: var(--font-ui);
  font-size: var(--fs-2xs);
  color: var(--ink-soft);
  max-width: 48ch;
}

/* ---- end matter ---------------------------------------------------------- */

.piecefoot {
  margin-top: var(--sp-9);
  padding-top: var(--sp-6);
  border-top: 2px solid var(--ink);
  max-width: var(--measure-wide);
}

.piecefoot h2 {
  font-size: var(--fs-xl);
  font-weight: 700;
  margin-bottom: var(--sp-4);
}

.piecefoot p {
  font-size: var(--fs-md);
  line-height: var(--lh-read);
  max-width: 56ch;
}

.piecefoot p + p {
  margin-top: var(--sp-4);
}

/* ==========================================================================
   MOTION
   --------------------------------------------------------------------------
   Section reveals only, plus hover and focus feedback. Nothing moves that a
   reader needs in order to read.

   FAIL-OPEN: the hidden state is scoped to html.has-js, which an inline script
   in <head> sets before first paint. Without JS nothing is ever hidden, so a
   script error cannot make content — least of all a disclosure — permanently
   invisible. main.js also unobserves each element after it fires, and re-scans
   after any content injection, because an observer that collects its targets
   once never sees anything added later.
   ========================================================================== */

.has-js .reveal {
  opacity: 0;
  transform: translateY(var(--reveal-lift));
  transition: opacity var(--dur-reveal) var(--ease-out),
    transform var(--dur-reveal) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}

.has-js .reveal.is-in {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* Staggered children. Capped at five steps: at 90ms a six-item list already
   takes 450ms to finish arriving, and beyond that the last item reads as
   broken rather than choreographed. */

.stagger > *:nth-child(1) { --reveal-delay: 0ms; }
.stagger > *:nth-child(2) { --reveal-delay: 90ms; }
.stagger > *:nth-child(3) { --reveal-delay: 180ms; }
.stagger > *:nth-child(4) { --reveal-delay: 270ms; }
.stagger > *:nth-child(5) { --reveal-delay: 360ms; }
.stagger > *:nth-child(n + 6) { --reveal-delay: 450ms; }

/* Hover gating. Everything that responds to a pointer lives in here, so a
   touch device never gets a stuck :hover state after a tap. */

@media (hover: hover) and (pointer: fine) {
  .masthead__link:hover {
    color: var(--ink);
    border-bottom-color: var(--teal);
  }

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

  .btn--solid:hover {
    background: var(--teal-ink);
    border-color: var(--teal-ink);
    color: var(--paper);
  }

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

  .btn:hover .btn__arrow {
    transform: translateX(4px);
  }

  /* The row lifts nothing and moves nothing. It warms. Movement in a list of
     links makes the target you are aiming at leave, which for a hand with a
     tremor is the difference between one click and three. */
  .piece__link:hover {
    background: var(--paper-sunk);
  }

  .piece__link:hover .piece__more span {
    transform: translateX(4px);
  }

  .link:hover,
  .prose p a:hover,
  .prose li a:hover {
    background: rgba(20, 207, 198, 0.16);
    border-bottom-color: var(--teal-ink);
  }

  .foot a:hover {
    color: var(--paper);
    border-bottom-color: var(--teal);
  }

  /* Hover is an ACCELERATOR on the media card, never the mechanism — the
     button is the mechanism and it exists on every device. Gated on
     :not(.is-swapped) so that once a reader has pinned the second image with
     the button, moving the mouse does not argue with him about it. */

  .mediacard:not(.is-swapped):hover .mediacard__img--b,
  .mediacard:not(.is-swapped):focus-within .mediacard__img--b {
    opacity: 1;
    visibility: visible;
    transition-delay: 0s;
  }

  .mediacard:not(.is-swapped):hover .mediacard__img--a,
  .mediacard:not(.is-swapped):focus-within .mediacard__img--a {
    opacity: 0;
    visibility: hidden;
    transition-delay: 0s, var(--dur-ui);
  }

  .mediacard__swap:hover {
    background: var(--ink);
    color: var(--paper);
  }

  /* Scoped with :has() so the underline only appears on a card that is
     actually clickable. A media card whose piece is not written yet has no
     .mediacard__link, and until 2026-08-31 it still underlined its title on
     hover -- a promise of a click that did nothing. :has() is already relied
     on a few rules above for the focus ring, so this adds no new support
     assumption, and it raises this selector's specificity rather than
     lowering it, so nothing it used to beat now beats it. */
  .mediacard:has(.mediacard__link):hover .mediacard__title {
    text-decoration: underline;
    text-decoration-color: var(--teal);
    text-decoration-thickness: 2px;
    text-underline-offset: 3px;
  }
}

/* --------------------------------------------------------- reduced motion */

/* No exceptions. For this audience vestibular sensitivity is not an edge case.
   Reveals become an instant no-op rather than a fade, and transitions on
   colour are kept, because a colour change is not motion.

   This block used to carry a fourth rule: the hero video did not play at all,
   and main.js read the same query and left the poster in place. Both are gone
   with the footage. The homepage hero is type now and there is nothing in it
   that moves, which is a better answer to the same problem than switching
   something off was. */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .has-js .reveal {
    opacity: 1;
    transform: none;
    transition: none;
    transition-delay: 0ms;
  }

  .btn:active {
    transform: none;
  }

  .btn__arrow,
  .piece__more span,
  .skip-link {
    transition: none;
  }

  /* The card swap still WORKS under reduced motion — it carries information,
     and suppressing it would remove a feature rather than an effect. Only the
     crossfade goes: the images cut. */

  .mediacard__img--a,
  .mediacard__img--b {
    transition: none;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

/* ------------------------------------------------------------------- print */

/* Readers in this bracket print things. An article that prints as a black
   rectangle with no disclosure is both rude and non-compliant. */

@media print {
  .masthead,
  .foot,
  .outbound .btn,
  .skip-link {
    display: none;
  }

  /* .hero__media and .hero__scrim used to be hidden here. They are gone with
     the video hero; the typographic hero is type and prints as type. */

  body {
    background: #fff;
    color: #000;
    font-size: 12pt;
  }

  .prose,
  .measure,
  .breakout,
  .skip {
    max-width: none;
  }

  .prose p a::after {
    content: " (" attr(href) ")";
    font-size: 9pt;
    color: #444;
  }
}

/* ==========================================================================
   AFFILIATE DISCLOSURE
   --------------------------------------------------------------------------
   Pattern docs, wording and placement rules: DISCLOSURE-PATTERN.md
   Markup to copy: disclosure-snippet.html
   Executable checks: test/disclosure-check.js  (run it after ANY edit here)

   Implements the "clear and conspicuous" test at 16 CFR 255.0(f) — a visual
   disclosure must stand out "by its size, contrast, location" and, in an
   interactive medium, must be UNAVOIDABLE. Four rules below are load-bearing
   and must not be "tidied":

   1. font-size is never below 15px — and on this site never below 18px, because
      the article body it qualifies is now 18.5-20px and a disclosure set
      smaller than the copy it qualifies does not "stand out by size". The 15px
      legal floor and the 18px site floor are BOTH asserted in
      test/disclosure-check.js. Do not shrink either to make a layout fit.
      (Archivo is used here rather than the reading serif: at the same nominal
      size its larger x-height makes the block read as louder than the prose
      around it, which is the direction 255.0(f) requires.)
   2. Text colour is #1f1f1f on --paper-sunk (#f1efea) — MEASURED 14.34:1,
      lead 9.45:1, links 17.23:1. Never --teal/--volt (#14cfc6) as text: it is
      1.87:1 on paper, far below the 4.5:1 AA floor. The accent is a BORDER.
   3. Nothing here hides, collapses or defers the text. No accordion, no
      tooltip, no "show more". 16 CFR 465.1(c)(4): a disclosure is not clear
      and conspicuous if the reader must click or hover to see it.
   4. NEW, and specific to this stylesheet: a .disclose block is never placed
      inside a .reveal container. .reveal sets opacity:0 until an observer
      fires, which would make the disclosure invisible if the script failed.
      test/disclosure-check.js asserts this against the HTML templates.

   The ground moved from the old #ececed to #f1efea when the palette warmed.
   That is a STRENGTHENING, not a weakening: the measured text contrast went
   from 13.96:1 to 14.34:1. The numbers above were computed by
   test/contrast-check.js, not estimated. */

.disclose {
  margin: 0 0 22px;
  padding: 14px 16px;
  border-left: 3px solid var(--volt-deep);
  background: var(--paper-sunk);
  color: #1f1f1f;
  font-family: var(--font-ui);
  font-size: 18px;
  line-height: 1.6;
  text-transform: none;
  letter-spacing: normal;
  max-width: 52rem;
}

.disclose p { margin: 0; }

.disclose p + p { margin-top: 8px; }

/* The lead label was 13px, and test/disclosure-check.js carried an explicit
   exemption for it: "the __lead is a small-caps label above the text, not the
   text". That reasoning is sound and the exemption was still worth deleting.

   Two reasons. The reader is 45-65 and presbyopic, and 13px uppercase with
   0.08em tracking is the hardest combination on the page to read at arm's
   length -- it is the signpost that says a commission is involved, so it is
   the last thing that should be squinted at. And an exemption inside a
   compliance check is a hole that stays open: the next person who needs a
   smaller size in this component has a precedent, and the precedent does not
   record why it was safe.

   At 15px the exemption is unnecessary, so it was removed, and the check is
   now absolute: nothing in .disclose is below 15px, no exceptions. Contrast is
   unchanged and still measured -- 9.45:1 on the sunk ground. */

.disclose__lead {
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 15px;
  display: block;
  margin-bottom: 6px;
  color: #3d3d3d;
}

/* Compact variant: sits directly above a link further down the page, where the
   full block has already run once at the top. Same size, same contrast — only
   the padding is smaller. */

.disclose--inline {
  margin: 0 0 10px;
  padding: 10px 14px;
}

/* The inline variant simply omits the __lead element (see disclosure-snippet.html
   case (e)). It is NOT hidden with display:none — there is deliberately no
   hiding property anywhere in this component, so that a future edit cannot
   retarget one onto the disclosure text itself. */

/* Links inside a disclosure stay visibly links, and stay legible on the sunk
   ground: #0a0a0a on #f1efea is 17.23:1. */

.disclose a {
  color: var(--ink);
  border-bottom: 1px solid currentColor;
}

@media (max-width: 860px) {
  /* Padding tightens; the type does not. See rule 1 above. */
  .disclose { padding: 13px 14px; }
  .disclose--inline { padding: 10px 12px; }
}
/* ==========================================================================
   HOMEPAGE VIDEO HERO — restored on branch restore/hero-scroll
   --------------------------------------------------------------------------
   Grafted from the 2026-08-30 original (full-bleed looping hero.mp4, copy
   bottom-left over the lifter, Archivo 900 italic, volt #14cfc6). Current
   publication styles above are left intact: .herob stays for the unused
   newspaper treatment, body/article type stays Source Serif, other pages
   keep .masthead. This block only styles the homepage .bar / .hero / .ticker.
   Shop / cart / Stripe rules are not restored.
   ========================================================================== */

@font-face {
  font-family: Archivo;
  src: url("fonts/archivo-latin-italic-900.woff2") format("woff2");
  font-weight: 900;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  --bar-h: 64px;
}

.bar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 60;
  height: var(--bar-h);
  background: var(--ink);
  color: var(--paper);
}

/* Homepage title card: logo/nav sit ON the video, not in a nameplate.
   Article pages keep the solid ink bar. */
html.home .bar {
  background: transparent;
  transition: background 180ms ease;
}
html.home.is-bar-solid .bar {
  background: var(--ink);
}


.bar__in {
  height: 100%;
  display: flex;
  align-items: center;
  gap: 32px;
  padding: 0 var(--gutter);
}

.bar .mark {
  color: var(--paper);
}

.bar .mark__compact {
  width: 36px;
  height: 36px;
  margin-left: -2.4px;
}

@media (min-width: 768px) {
  .bar .mark__lockup {
    width: 220px;
    height: 42.7px;
    margin-left: -5.3px;
  }
}

@media (min-width: 1100px) {
  .bar .mark__lockup {
    width: 254px;
    height: 49.3px;
    margin-left: -6.1px;
  }
}

.bar__nav {
  display: flex;
  gap: 26px;
  margin-left: auto;
}

.bar__link {
  position: relative;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 4px 0;
  text-decoration: none;
  color: var(--paper);
}

.bar__link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 2px;
  background: var(--volt);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-hover) var(--ease-out);
}

@media (max-width: 860px) {
  .bar__nav { gap: 16px; }
}

@media (max-width: 429px) {
  .bar__nav { gap: 12px; }
}

.hero {
  position: relative;
  height: 100svh;
  min-height: 560px;
  overflow: hidden;
  background: var(--ink);
}

.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
}

.hero__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, rgba(10, 10, 10, 0.72) 0%, rgba(10, 10, 10, 0.32) 40%, rgba(10, 10, 10, 0) 70%), linear-gradient(180deg, rgba(10, 10, 10, 0.4) 0%, rgba(10, 10, 10, 0) 42%, rgba(10, 10, 10, 0.45) 100%);
}

.hero__cue {
  position: absolute;
  left: 50%;
  bottom: 34px;
  transform: translateX(-50%);
  display: grid;
  justify-items: center;
  gap: 10px;
  color: var(--paper);
  mix-blend-mode: difference;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.hero__mouse {
  width: 20px;
  height: 32px;
  border: 1.5px solid currentColor;
  border-radius: 12px;
  position: relative;
}

.hero__mouse::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 7px;
  width: 2px;
  height: 6px;
  margin-left: -1px;
  background: currentColor;
  animation: hero-wheel 1.6s var(--ease-in-out) infinite;
}

@keyframes hero-wheel {
  0%, 100% { transform: translateY(0); opacity: 1; }
  50% { transform: translateY(8px); opacity: 0.35; }
}

.hero__copy {
  position: absolute;
  left: var(--gutter);
  right: var(--gutter);
  top: var(--bar-h);
  bottom: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding-bottom: clamp(84px, 13vh, 170px);
  color: var(--paper);
  text-shadow: 0 1px 24px rgba(10, 10, 10, 0.55), 0 1px 3px rgba(10, 10, 10, 0.4);
}

.hero__kicker {
  margin-bottom: 18px;
  color: var(--paper);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.hero__head {
  margin: 0 0 22px;
  max-width: 16ch;
  font-family: Archivo, "Helvetica Neue", Arial, sans-serif;
  font-weight: 900;
  font-style: italic;
  text-transform: uppercase;
  letter-spacing: -0.035em;
  line-height: 0.86;
  font-size: min(clamp(40px, 7.4vw, 118px), 17vh);
  color: var(--paper);
  text-wrap: wrap;
}

.hero__sub {
  max-width: 40ch;
  margin: 0 0 26px;
  font-family: Archivo, "Helvetica Neue", Arial, sans-serif;
  font-size: clamp(15px, 1.5vw, 18px);
  font-weight: 400;
  font-style: normal;
  line-height: 1.45;
  color: var(--paper);
  text-wrap: wrap;
}

@media (max-height: 700px) {
  .hero__copy { padding-bottom: 64px; }
  .hero__sub { display: none; }
}

.hero__acts { display: flex; gap: 12px; flex-wrap: wrap; }

.pill {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 13px 26px;
  border: 1.5px solid var(--ink);
  border-radius: 999px;
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition:
    background-color var(--dur-hover) ease,
    color var(--dur-hover) ease;
}

.pill svg { transition: transform var(--dur-hover) var(--ease-out); }

.pill--light { border-color: var(--paper); color: var(--paper); }

.pill:active { transform: scale(0.98); transition-duration: 100ms; }

.ticker {
  background: var(--volt);
  color: var(--ink);
  padding: 14px 0;
  overflow: hidden;
  white-space: nowrap;
}

.ticker__track {
  display: inline-flex;
  gap: 40px;
  padding-left: 40px;
  animation: hero-ticker 26s linear infinite;
  font-family: Archivo, "Helvetica Neue", Arial, sans-serif;
  font-weight: 900;
  font-style: italic;
  font-size: 15px;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

@keyframes hero-ticker {
  to { transform: translateX(-50%); }
}

@media (hover: hover) and (pointer: fine) {
  .bar__link:hover::after { transform: scaleX(1); }
  .pill:hover { background: var(--ink); color: var(--paper); }
  .pill--light:hover { background: var(--paper); color: var(--ink); }
  .pill:hover svg { transform: translateX(4px); }
  .ticker:hover .ticker__track { animation-play-state: paused; }
}

@media (max-width: 860px) {
  .hero__copy { bottom: clamp(80px, 14vh, 140px); }
}

@media (prefers-reduced-motion: reduce) {
  .hero__mouse::after,
  .ticker__track { animation: none; }
}

/* ==========================================================================
   CINEMATIC HOMEPAGE — branch homepage/cinematic-scroll
   --------------------------------------------------------------------------
   Upgrades the restored looping hero; does not stack a second one.
   Spec: HOMEPAGE.md. Two effects: ambient loop (.hero) and pinned scrub
   (.cinema). Article copy after that sits on paper with no video behind it.
   ========================================================================== */

.hero__copy {
  position: absolute;
  inset: var(--bar-h) 0 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 var(--gutter) 96px;
  color: var(--paper);
  text-shadow: 0 1px 24px rgba(10, 10, 10, 0.55), 0 1px 3px rgba(10, 10, 10, 0.4);
}

.hero__rom {
  width: 68px;
  height: 68px;
  margin: 0 0 22px;
  color: var(--paper);
}

.hero__brand {
  margin: 0 0 18px;
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--paper);
}

.hero__head {
  margin: 0;
  max-width: 14ch;
  font-family: Archivo, "Helvetica Neue", Arial, sans-serif;
  font-weight: 900;
  font-style: italic;
  text-transform: none;
  letter-spacing: -0.03em;
  line-height: 1.04;
  font-size: clamp(28px, 5.2vw, 68px);
  color: var(--paper);
}

.hero__cue {
  text-decoration: none;
  color: var(--paper);
}

/* ---- pinned cinematic -------------------------------------------------- */

.cinema {
  position: relative;
  height: 420vh;
  background: var(--ink);
}

.cinema__pin {
  position: sticky;
  top: 0;
  height: 100svh;
  min-height: 560px;
  overflow: hidden;
  background: var(--ink);
}

.cinema__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
}

.cinema__scrim {
  position: absolute;
  inset: 0;
  background: rgba(10, 10, 10, 0.15);
  pointer-events: none;
}

.cinema__copy {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--gutter);
  pointer-events: none;
}

.cinema__line {
  margin: 0;
  max-width: 10ch;
  text-align: center;
  font-family: Archivo, "Helvetica Neue", Arial, sans-serif;
  font-weight: 900;
  font-style: italic;
  text-transform: uppercase;
  letter-spacing: -0.045em;
  line-height: 0.88;
  font-size: clamp(36px, 8.2vw, 118px);
  color: var(--paper);
  text-shadow: 0 2px 32px rgba(10, 10, 10, 0.65);
  will-change: transform, opacity;
}

/* Paper takes over. No video behind the long copy. */
.hero ~ .section,
.cinema ~ .section,
.cinema ~ .foot {
  position: relative;
  z-index: 2;
  background: var(--paper);
}

.cinema ~ .section.section--sunk { background: var(--paper-sunk); }
.cinema ~ .section.section--ink { background: var(--ink); }

/* ---- magazine grid on the homepage ------------------------------------- */

.home-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  list-style: none;
  padding: 0;
  margin: 0;
  border-top: 1px solid var(--rule-strong);
}

.home-grid__lead,
.home-grid__tile {
  display: block;
  text-decoration: none;
  color: inherit;
  padding: 28px 0;
  border-bottom: 1px solid var(--rule);
}

.home-grid__lead { padding-top: 36px; padding-bottom: 36px; }

.home-grid__title {
  margin: 8px 0 10px;
  font-family: var(--font-ui);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.15;
  font-size: clamp(22px, 3vw, 34px);
  color: var(--ink);
}

.home-grid__stand {
  margin: 0;
  max-width: 52ch;
  font-family: var(--font-text);
  font-size: var(--fs-md, 18px);
  line-height: 1.5;
  color: var(--ink-body);
}

.home-grid__lead .home-grid__title {
  font-size: clamp(28px, 4.2vw, 48px);
  max-width: 18ch;
}

.who--slim .who__body { max-width: 46ch; }
.who--slim .who__portrait { display: none; }

@media (min-width: 860px) {
  .home-grid {
    grid-template-columns: 1.4fr 1fr 1fr;
    gap: 0 36px;
    align-items: stretch;
  }
  .home-grid__lead {
    grid-row: 1;
    padding-right: 28px;
    border-right: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
  }
  .home-grid__tile { padding-top: 36px; }
}

@media (hover: hover) and (pointer: fine) {
  .home-grid__lead:hover .home-grid__title,
  .home-grid__tile:hover .home-grid__title {
    text-decoration: underline;
    text-decoration-color: var(--teal);
    text-decoration-thickness: 2px;
    text-underline-offset: 4px;
  }
  .hero__cue:hover { color: var(--volt); }
}

@media (max-height: 700px) {
  .hero__copy { padding-bottom: 72px; }
  .hero__rom { width: 52px; height: 52px; margin-bottom: 14px; }
}

@media (max-width: 860px) {
  .hero__copy { bottom: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .hero__mouse::after,
  .ticker__track { animation: none; }

  .hero__video,
  .cinema__video { display: none; }

  .hero,
  .cinema__pin {
    background-color: var(--ink);
    background-image: url("hero-poster.jpg");
    background-size: cover;
    background-position: center;
  }

  .cinema { height: auto; }
  .cinema__pin { position: relative; }
  .cinema__line {
    opacity: 1;
    transform: none;
    will-change: auto;
  }
  .cinema__scrim { background: rgba(10, 10, 10, 0.45); }
}

@media print {
  .hero__video,
  .cinema__video,
  .hero__cue { display: none; }
  .hero,
  .cinema,
  .cinema__pin {
    height: auto;
    min-height: 0;
    position: static;
    background: #000;
    color: #fff;
    padding: 24pt 0;
  }
}

.pieces--quiet {
  margin-top: 12px;
  padding-top: 8px;
}
.pieces--quiet .piece__title {
  font-size: 1.05em;
}
.pieces--quiet .piece__stand {
  max-width: 62ch;
}

/* Load more — wired at the seventh published piece. Fail-open: without JS
   every piece is in the document and visible, and the button is not shown. */
.loadmore {
  display: none;
  margin-top: var(--sp-7);
}
html.has-js .loadmore { display: block; }
html.has-js .piece--older { display: none; }
html.has-js .pieces.is-open .piece--older { display: list-item; }
html.has-js .pieces.is-open + .loadmore,
html.has-js .loadmore.is-done { display: none; }

/* Homepage magazine closer — one lead, a short list, room to breathe.
   Replaces the stacked home-grid that read as a kitchen inventory. */
.home-mag {
  padding-top: clamp(48px, 8vw, 88px);
  padding-bottom: clamp(40px, 6vw, 72px);
}
.home-mag a {
  text-decoration: none;
  color: inherit;
}
.home-mag__lead {
  display: grid;
  gap: 28px;
  padding-bottom: 40px;
  border-bottom: 1px solid var(--rule);
}
.home-mag__frame img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--paper-sunk);
}
.home-mag__body time {
  font-family: var(--font-ui);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--teal-ink);
}
.home-mag__body h2 {
  margin: 14px 0 16px;
  max-width: 16ch;
  font-family: var(--font-text);
  font-size: clamp(28px, 3.6vw, 44px);
  font-weight: 700;
  line-height: 1.12;
  letter-spacing: -0.02em;
  color: var(--ink);
  text-decoration: none;
}
.home-mag__body p {
  margin: 0;
  max-width: 42ch;
  color: var(--ink-body);
}
.home-mag__lead:hover h2 {
  color: var(--teal-ink);
}
.home-mag__list {
  list-style: none;
  margin: 0;
  padding: 4px 0 0;
}
.home-mag__list a {
  display: grid;
  grid-template-columns: 1fr;
  gap: 6px;
  padding: 22px 0;
  border-bottom: 1px solid var(--rule);
}
.home-mag__list time {
  font-family: var(--font-ui);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.home-mag__list span {
  font-family: var(--font-text);
  font-size: clamp(18px, 2.1vw, 22px);
  font-weight: 650;
  line-height: 1.3;
  color: var(--ink);
}
.home-mag__list a:hover span {
  color: var(--teal-ink);
}
.home-mag__more {
  margin: 32px 0 0;
}
.home-mag__more a {
  font-family: var(--font-ui);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 2px solid var(--teal);
  padding-bottom: 3px;
}
.home-mag__more a:hover {
  color: var(--teal-ink);
}
@media (min-width: 860px) {
  .home-mag__lead {
    grid-template-columns: 0.85fr 1.15fr;
    gap: 56px;
    align-items: center;
    padding-bottom: 56px;
  }
  .home-mag__list a {
    grid-template-columns: 9.5rem 1fr;
    gap: 28px;
    align-items: baseline;
    padding: 26px 0;
  }
}



/* Hero left rail: the drawn box. Four stories, own scroll, video stays. */
section.hero .hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  z-index: 0;
}
section.hero .hero__scrim {
  z-index: 1;
  background:
    linear-gradient(90deg, rgba(10, 10, 10, 0.55) 0%, rgba(10, 10, 10, 0.22) 38%, rgba(10, 10, 10, 0) 62%),
    linear-gradient(180deg, rgba(10, 10, 10, 0.4) 0%, rgba(10, 10, 10, 0) 42%, rgba(10, 10, 10, 0.4) 100%);
}
section.hero .hero__rail {
  position: absolute;
  left: var(--gutter);
  top: calc(var(--bar-h) + 20px);
  bottom: 36px;
  width: min(22rem, 34vw);
  z-index: 4;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 22px 22px 18px;
  color: var(--paper);
  background: rgba(10, 10, 10, 0.52);
  border: 1px solid rgba(251, 250, 248, 0.28);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  text-shadow: 0 1px 12px rgba(10, 10, 10, 0.55);
  scrollbar-width: thin;
  scrollbar-color: rgba(251, 250, 248, 0.35) transparent;
}
section.hero .hero__rail::-webkit-scrollbar { width: 6px; }
section.hero .hero__rail::-webkit-scrollbar-thumb {
  background: rgba(251, 250, 248, 0.35);
  border-radius: 99px;
}
section.hero .hero__rail-kicker {
  margin: 0 0 14px;
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--paper);
}
section.hero .hero__rail-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
section.hero .hero__rail-list li + li {
  border-top: 1px solid rgba(251, 250, 248, 0.18);
}
section.hero .hero__rail-list a {
  display: block;
  padding: 14px 0;
  color: var(--paper);
  text-decoration: none;
}
section.hero .hero__rail-list strong {
  display: block;
  font-family: var(--font-text);
  font-size: clamp(16px, 1.4vw, 19px);
  font-weight: 700;
  line-height: 1.25;
  margin: 0 0 6px;
}
section.hero .hero__rail-list span {
  display: block;
  font-size: 14px;
  line-height: 1.4;
  color: rgba(251, 250, 248, 0.82);
}
section.hero .hero__rail-list a:hover strong { color: var(--teal); }
@media (max-width: 700px) {
  section.hero .hero__rail {
    width: auto;
    right: var(--gutter);
    bottom: 72px;
  }
}
section.hero .hero__copy { display: none; }
section.hero .hero__hooks { display: none; }
