/* ============================================================
   tacttus.css — GENERATED. Do not edit.

   Built by scripts/build_css.py from design-system/ in the declared
   order below. Edit the source file, run the script, commit both.
   `python3 scripts/build_css.py --check` fails when this file and a
   fresh build differ.
   ============================================================ */

/* ==== design-system/tokens.css ==== */
/* ============================================================
   Tacttus — design tokens
   The Block System. Every value here is measured, not chosen by
   eye; the ratios in the comments are WCAG contrast and are
   load-bearing. Changing a colour means re-measuring the pairs.
   ============================================================ */

:root{
  /* ---- the module -------------------------------------------------
     One token governs every space on the page. New spacing is a
     multiple of this, never a fresh clamp. 20.88px at 1440.        */
  --mod: clamp(14px, 1.45vw, 22px);
  /* the ONE fluid spacing token. A fixed 4-module gutter left a 267px
     column on a 390px phone, so below ~965px it follows 5.8vw down to
     a 20px floor. Scoped exception to the Module Rule; not precedent. */
  --gutter: clamp(20px, 5.8vw, calc(var(--mod) * 4));
  --gap:      var(--mod);
  --gap-lg:   calc(var(--mod) * 2);
  --pad-band: calc(var(--mod) * 4);
  --pad-cell: calc(var(--mod) * 1.5);
  --head-gap: calc(var(--mod) * 2);

  /* ---- colour: six committed values -------------------------------
     No standalone greys. Anything else is derived from these.      */
  --ground:  #EDEEF0;   /* cool light ground, never pure white       */
  --ink:     #14161A;   /* text; 18.67:1 on ground                   */
  --growth:  #FF5C00;   /* growth. LARGE FIELDS ONLY (see rules)     */
  --product: #1B4DFF;   /* product                                   */
  --overlap: #1B1C00;   /* DERIVED: growth x product, per-channel
                           multiply. Not an authored colour.         */
  --white:   #FFFFFF;

  /* ---- derived tints ---------------------------------------------- */
  --ink-2:       #5E5F63;  /* 66% ink in ground. 5.49:1 ground / 6.38:1 white */
  --hair:        #ACADB0;  /* 30% ink in ground. intra-list rules only        */
  --accent-text: #B83D00;  /* the ONLY orange that passes as text: 4.88:1     */
  --on-product:  #EEF1FF;  /* 5.25:1 on product. secondary text on a blue field */
  --on-growth:   #3A2000;  /* 4.89:1 on growth                               */
  --on-overlap:  #E6E7DF;  /* 13.90:1 on overlap                             */
  --rule-dark:   var(--growth);  /* dividers on dark surfaces (footer)      */

  /* ---- type: one family, width and weight carry hierarchy ---------
     Archivo variable only. No serif, no monospace anywhere.
     Caps appear in the TACTTUS wordmark and nowhere else.          */
  --fs-micro:   .78rem;
  --fs-small:   .84rem;
  --fs-control: .92rem;
  --fs-list:    1rem;      /* list items inside a field or a panel */
  --fs-action:  1.02rem;
  --fs-row:     clamp(1.3rem,  2.6vw, 2.1rem);
  --fs-phase:   clamp(1.25rem, 1.6vw, 1.45rem);
  --fs-phase-n: clamp(2rem,    3.4vw, 3.2rem);
  --fs-headline:clamp(1.65rem, 3.3vw, 2.9rem);
  /* capped BELOW the thesis: a section heading never out-scales the h1
     (Thesis-Outranks-Sections). Display tops out at 6rem / 6.1vw. */
  --fs-mega:    clamp(2.2rem,  5.6vw, 4.6rem);

  /* ---- motion ------------------------------------------------------
     Strong curves only. The browser defaults are too weak.         */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}

/* Every token above is consumed inside design-system/: the fit cards
   (fit.css), the footer (footer.css) and the contact/booking flow
   (flow.css, form.css, panel.css, steps.css) were extracted in US-002,
   so --fs-list, --on-product and --rule-dark now have real consumers.
   This :root is token-for-token identical to the approved homepage
   wireframe's, and verify_site.py's token-parity check holds it there.
   --fs-fit was removed in the Sep 20 rebuild; do not restore it. */

/* ============================================================
   COLOUR RULES — these are not preferences

   ink on growth ................ 5.85:1  use this
   white on growth .............. 3.10:1  FAILS. never ship it.
   growth as text on ground ..... 2.92:1  FAILS. orange is a field.
   growth field vs ground ....... 2.67:1  so never a small graphic
                                          element; use --accent-text
   white on product ............. 5.91:1
   ink on product ............... 3.06:1  large text only
   ink on overlap ............... 1.05:1  nothing inked on overlap
   growth on overlap ............ 5.59:1  orange reads on the dark
   product on overlap ........... 2.93:1  FAILS. no blue on dark.
   ============================================================ */

/* ==== design-system/base.css ==== */
/* ============================================================
   Tacttus — base
   Browser surfaces, the type roles, the page container, bands, and
   the shared motion keyframes. Requires tokens.css.

   Every value here is taken from the approved wireframes:
   .impeccable/wireframes/tacttus-v2-modern.dc.html (the reference
   surface), redesign-contact.dc.html and redesign-booking.dc.html.
   Where a wireframe and an older design-system value disagreed, the
   wireframe won.

   Type: Archivo variable only, 'wdth' 75..125, 'wght' 400..900.
   Self-hosted since US-003 (D13): the @font-face below is the only
   place the font is declared, so a page links this bundle and nothing
   else, and no font request leaves this origin. The gate is a literal
   string search, so do not name the third-party font hosts in here
   either — a comment mentioning them fails it exactly like a live link.
   ============================================================ */

/* ---- the one typeface -------------------------------------------
   D13: one variable woff2, latin subset, served from this origin. The
   src is ROOT-ABSOLUTE on purpose: this rule ships inside the bundle at
   /design-system/tacttus.css, and a relative url() would resolve against
   /design-system/, not the site root. Root-absolute resolves from the
   bundle, from a component file loaded directly, and from any page depth.

   🔑 The standalone mirror (m-honorato/tacttus-design) has no /assets at
   its root, so the gallery opened from that checkout falls back to
   system-ui until US-009 handles the mirror's copy of the font. That is a
   known, scoped fallback, not a bug.

   font-display:swap (D13): text paints in the fallback immediately and
   re-paints in Archivo. The variable ranges are declared so the browser
   knows one file answers every weight and width the system asks for —
   without them it synthesises bold and condensed instead. */
@font-face{
  font-family:'Archivo';
  src:url('/assets/fonts/archivo-variable.woff2') format('woff2');
  font-weight:400 900;
  font-stretch:75% 125%;
  font-style:normal;
  font-display:swap;
  /* latin, matching the subset Google Fonts serves for this axis range */
  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;
}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}

body{
  margin:0;background:var(--ground);color:var(--ink);
  font-family:'Archivo',system-ui,sans-serif;
  font-variation-settings:'wdth' 100,'wght' 400;
  font-size:clamp(15px,0.55vw + 13px,17px);
  line-height:1.5;text-wrap:pretty;-webkit-font-smoothing:antialiased;
  caret-color:var(--product);
  scrollbar-color:var(--ink) var(--ground);scrollbar-width:thin;
}

/* the masthead is sticky, so a nav jump has to land BELOW it and not
   under it. Every [id] on every page, not per-section. */
[id]{scroll-margin-top:calc(var(--mod) * 5)}

/* the browser's own surfaces are part of the identity, not defaults */
::selection{background:var(--growth);color:var(--ink)}
::-webkit-scrollbar{width:11px;height:11px}
::-webkit-scrollbar-track{background:var(--ground)}
::-webkit-scrollbar-thumb{background:var(--ink)}

/* Ring-Pair. The PAIR guarantees visibility; a single component's worst
   case is 1.00:1 (white ring on white card / ink ring on ink band).
   NEVER drop either the outline or the box-shadow. */
:focus-visible{outline:3px solid var(--white);outline-offset:0;box-shadow:0 0 0 6px var(--ink)}
@media (forced-colors:active){:focus-visible{outline:3px solid CanvasText}}

a{color:inherit;text-decoration-thickness:2px;text-underline-offset:.22em;
  text-decoration-color:var(--accent-text)}

/* skip link — off-screen until focused, then an ink field at the gutter.
   It moves with `top`, not with a transform, so it is never a 0-opacity
   box extending the scroll area. */
.skip{position:absolute;left:var(--gutter);top:-100px;z-index:99;
  background:var(--ink);color:var(--white);padding:12px 16px;text-decoration:none}
.skip:focus{top:8px}

/* ---- the one container ------------------------------------------
   Gutter-Owner: .sheet owns the page's horizontal padding. Nothing
   nested inside it adds its own.                                   */
.sheet{max-width:1560px;margin:0 auto;padding-inline:var(--gutter)}

/* ---- bands ------------------------------------------------------
   A band is a full-bleed horizontal slice. Its children sit in a
   .sheet. Only two light grounds: the page ground, or white.       */
.band{padding-block:var(--pad-band)}
.band--tint{background:var(--white)}
.band__head{max-width:52ch;margin-bottom:var(--head-gap)}
.band__lede{font-size:clamp(1.02rem,1.2vw,1.18rem);line-height:1.45;max-width:52ch;
  margin:var(--gap) 0 0;color:var(--ink-2)}

/* ---- type roles -------------------------------------------------
   Width and weight carry the hierarchy, not a second family.
   Caps exist only in the wordmark. The tracking floor is -.04em;
   nothing in the system is set tighter.                            */
.display,.h2,.mega{overflow-wrap:break-word}

/* Display — the thesis, one per page. line-height 1.0 is load-bearing:
   a .bar field is line-height + 2 x inset, so the tight setting is what
   makes the crossing between two lines deep (Overshoot Rule). */
.display{font-variation-settings:'wdth' 112,'wght' 800;
  font-size:clamp(2.4rem,6.1vw,6rem);line-height:1.0;letter-spacing:-.04em;margin:0}

/* Display-alt — the declared variant for a task page whose headline runs
   beside a panel (contact, booking). 'wdth' 112 is kept; only the size
   drops, so /contact never out-shouts the homepage thesis. */
.display--alt{font-size:clamp(2.15rem,5.2vw,4.6rem);text-wrap:balance}

.mega{font-variation-settings:'wdth' 112,'wght' 800;
  font-size:var(--fs-mega);line-height:.98;letter-spacing:-.04em;margin:0;text-wrap:balance}

.h2{font-variation-settings:'wdth' 108,'wght' 750;
  font-size:var(--fs-headline);line-height:1.05;letter-spacing:-.035em;margin:0;
  max-width:26ch;text-wrap:balance}

.lede{font-size:clamp(1.12rem,1.45vw,1.4rem);line-height:1.45;max-width:46ch;margin:0}
/* the flow variant: /contact and /booking set their lede wider and give
   it its own top margin, because nothing else sits between it and the h1 */
.lede--task{max-width:50ch;margin:var(--gap-lg) 0 0}

.small{font-size:var(--fs-small);line-height:1.4;color:var(--ink-2)}
.num{font-variant-numeric:tabular-nums lining-nums}

/* ---- shared motion keyframes ------------------------------------
   Declared once, here, and used by bar.css, duo.css and panel.css.
   They live inside prefers-reduced-motion:no-preference, and so do
   every animation declaration that references them — the pair
   matches or neither exists, which is what makes reduced motion
   render the finished page rather than an animation that cannot run.
   Only transform, opacity, clip-path and one stepped colour change:
   never a field's paint (Legible-At-Rest).                         */
@media (prefers-reduced-motion:no-preference){
  @keyframes wipe-l{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}
  @keyframes wipe-r{from{clip-path:inset(0 0 0 100%)}to{clip-path:inset(0 0 0 0)}}
  /* white-on-blue text stays INK until its field has arrived, so it is
     never white on the ground. One steps(1,end) change, not a fade. */
  @keyframes ink-then-white{from{color:var(--ink)}to{color:var(--white)}}
  @keyframes rise{from{opacity:0;transform:translateY(calc(var(--mod) * .9))}to{opacity:1;transform:none}}
  @keyframes fade{from{opacity:0}to{opacity:1}}
}

/* ============================================================
   RULES THIS FILE ENFORCES
   Zero-Radius ...... nothing is rounded except .mark__glyph, which
                      is a binding brand commitment.
   Flat-By-Construction  no shadows. Depth comes from a colour field
                      meeting another colour field.
   Gutter-Owner ..... only .sheet pads horizontally.
   Ring-Pair ........ the focus ring is two components, always both.
   Legible-At-Rest .. never paint a colour only inside an animation;
                      the resting state must already pass contrast.
   Declared-Step .... every small size comes from the five --fs-*
                      steps, every large one from a --fs-* token.
   ============================================================ */

/* ============================================================
   D17 — the phone layer

   One index.html serves both Claude Design sources: the desktop
   design above 640px, the phone design at 640 and below. The phone
   source is a fixed 430px column with its OWN token set, so the few
   tokens whose fluid value does not land on the phone's value are
   restated here and nowhere else. Everything derived from them
   follows, which is why this block is six declarations rather than a
   second stylesheet.

   Measured at 390px, which is the width the fidelity gate renders:

     --gutter   5.8vw = 22.62px, the phone draws 20px
     --gap      = --mod = 14px,  the phone draws 16px
     --fs-row       1.3rem,  the phone 1.45rem
     --fs-headline  1.65rem, the phone 1.85rem
     --fs-mega      2.2rem,  the phone 2.5rem
     body       0.55vw+13px = 15.15px, the phone 16px

   🔑 --mod itself is NOT restated. It is 14px at 390 against the
   phone's 16px, but the phone's spacings are authored values
   (44px, 22px, 34px) and not multiples of 16, so moving --mod would
   shift every padding in the system and then need overriding back one
   component at a time. --gap-lg (28px) and --pad-band (56px) already
   land on the phone's values at --mod:14px, so they are left alone.
   The remaining spacing differences are stated in the component that
   owns them, next to the rule they change.
   ============================================================ */
@media (max-width:640px){
  :root{
    --gutter:20px;
    --gap:16px;
    --fs-row:1.45rem;
    --fs-headline:1.85rem;
    --fs-mega:2.5rem;
  }
  body{font-size:16px}
  /* the thesis is set larger on the phone, not smaller: it is the whole
     first screen once the portraits are gone. isolation:isolate keeps the
     .bar fields' z-index:-1 inside the headline. */
  .display{font-size:clamp(2.35rem,11vw,3.2rem);position:relative;isolation:isolate}
  .lede{font-size:1.08rem}
  .band__lede{font-size:1.05rem}

  /* 🔑 THE MEASURE CAPS COME OFF, all of them, and this is the single
     biggest reason a phone render drifts from its source.
     The desktop design caps a measure in `ch` because its column is
     wider than a comfortable line. The phone's column IS the measure:
     350px of content inside a 390px screen. A 22ch heading cap is
     ~250px there, so a heading the phone source sets on one line wraps
     onto two, every element under it moves down, and the page ends up
     a hundred pixels long with nothing individually wrong. The phone
     source declares no max-width anywhere; neither does this. */
  .h2,.lede,.band__head,.band__lede{max-width:none}

  /* the two <br>s in the Mega headings are the DESKTOP source's: it breaks
     "From fundamentals / to handover" and "Who we work / best with" by hand
     at 1440. The phone source writes both as one string and lets
     text-wrap:balance place the break. One markup serves both by carrying
     the break and switching it off here.
     🔴 The markup writes a SPACE before each <br> for this rule to leave
     behind. Without it display:none removed the only separator between the
     two words and the phone read "From fundamentalsto handover". A space
     before a <br> collapses at the end of a line, so it costs the desktop
     nothing. */
  .mega br{display:none}
}

/* ==== design-system/components/mark.css ==== */
/* ============================================================
   mark — the logomark + wordmark lockup

   <a class="mark" href="/">
     <span class="mark__glyph mark__glyph--art" aria-hidden="true"></span>
     <span class="mark__word">TACTTUS</span>
   </a>
   On a dark ground, invert the glyph — colour flips, no second asset:
     .foot__brand .mark__glyph{background:var(--white);color:var(--ink)}

   assets/mark.svg     the full lockup glyph, standalone (ink square + t)
   assets/favicon.svg  the same, for <link rel="icon">
   assets/mark-t.svg   the letterform alone, used by the mask below

   No hover on the mark. The Sep 18 build wiggled the glyph; the rebuild
   removed it (Flat-By-Construction: hover changes colour, never height).
   ============================================================ */

.mark{display:flex;align-items:center;gap:calc(var(--mod) * .5);
  text-decoration:none;min-height:44px}

/* scoped exception to Zero-Radius: the rounded square is a binding
   brand commitment, not a UI decision. It is the only radius in the
   system. Do not generalise it to buttons or cards. */
.mark__glyph{
  width:calc(var(--mod) * 1.5);height:calc(var(--mod) * 1.5);
  background:var(--ink);color:var(--white);border-radius:6px;
  display:grid;place-items:center;
  /* the text path is the FALLBACK. Prefer .mark__glyph--art (D9).
     🔴 OPEN either way: this letterform is Georgia Italic, a system
     stand-in. A system display face is a craft-floor violation for an
     own-world page, and outlining it does not change that — it only
     removes the font dependency. Production needs a drawn or licensed
     glyph. The Mark-Serif Exception covers the intent, not this. */
  font-family:Georgia,'Times New Roman',serif;font-style:italic;
  font-size:calc(var(--mod) * .95);line-height:1;
}
.mark__word{font-variation-settings:'wdth' 100,'wght' 700;
  letter-spacing:.2em;font-size:var(--fs-micro)}

/* ---- D9, preferred: the outlined letterform ---------------------
   <span class="mark__glyph mark__glyph--art" aria-hidden="true"></span>
   The span stays EMPTY. The mark is painted with currentColor through
   a mask, so the inverted footer lockup needs no second asset and the
   glyph is identical on every machine regardless of installed fonts.
   Measured against the text version: 0.69% differing pixels at 240px,
   all of it antialiasing. */
.mark__glyph--art::before{
  content:'';width:100%;height:100%;background:currentColor;
  /* 🔑 the mask is a data URI, not url(../assets/mark-t.svg). An EXTERNAL
     svg mask is blocked on plain file:// — the square renders empty when
     someone opens index.html by double-clicking it. Verified: the external
     form works over http and with --allow-file-access-from-files, and fails
     without. assets/mark-t.svg stays the source of truth for this path;
     regenerate this URI from it if the letterform changes. */
  -webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath fill='%23000' d='M15.487 23.205Q14.56 23.205 14.13 22.829Q13.7 22.453 13.7 21.75Q13.7 21.525 13.729 21.325Q13.759 21.125 13.808 20.871L15.321 14.348H13.583L13.788 13.459H15.565L16.249 10.598H18.007L17.343 13.459H20.067L19.862 14.348H17.157L15.907 19.729Q15.819 20.1 15.761 20.446Q15.702 20.793 15.702 21.037Q15.702 21.574 15.971 21.823Q16.239 22.072 16.942 22.072Q17.196 22.072 17.655 22.009Q18.114 21.945 18.368 21.848L18.202 22.561Q17.519 22.824 16.913 23.015Q16.308 23.205 15.487 23.205Z'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
          mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath fill='%23000' d='M15.487 23.205Q14.56 23.205 14.13 22.829Q13.7 22.453 13.7 21.75Q13.7 21.525 13.729 21.325Q13.759 21.125 13.808 20.871L15.321 14.348H13.583L13.788 13.459H15.565L16.249 10.598H18.007L17.343 13.459H20.067L19.862 14.348H17.157L15.907 19.729Q15.819 20.1 15.761 20.446Q15.702 20.793 15.702 21.037Q15.702 21.574 15.971 21.823Q16.239 22.072 16.942 22.072Q17.196 22.072 17.655 22.009Q18.114 21.945 18.368 21.848L18.202 22.561Q17.519 22.824 16.913 23.015Q16.308 23.205 15.487 23.205Z'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}

/* D17: the phone source draws the lockup at a fixed 24px with an 8px
   gap. --mod is 14px at 390, so the module multiples land at 21px and
   7px — small, but the mark is the first thing on the page and the
   masthead's height follows it. */
@media (max-width:640px){
  .mark{gap:8px}
  .mark__glyph{width:24px;height:24px;font-size:15px}
  .foot__brand{gap:8px}
}

/* ==== design-system/components/masthead.css ==== */
/* ============================================================
   masthead — the sticky page header

   <span id="top"></span> immediately before it, and system.js, which
   toggles .is-stuck off that sentinel.

   <header class="masthead">
     <div class="sheet masthead__in">
       <a class="mark" href="/">…</a>
       <nav class="nav" aria-label="Main">
         <a href="/#work">Services</a>
         <a class="btn btn--outline" href="/contact">Book a call</a>
       </nav>
     </div>
   </header>

   Solid ground, never glass: the Sep 18 build's backdrop-filter blur and
   82% translucency were removed in the rebuild. When the page has
   scrolled, a 2px ink rule appears on the lower edge — that rule, not a
   shadow, is what separates the header from the page.

   .masthead--ruled is the variant the two flow pages use (/contact,
   /booking): they carry the rule at rest, because they are one screen of
   work with no hero to sit over. Same component, no sentinel needed.
   ============================================================ */

.masthead{
  position:sticky;top:0;z-index:50;background:var(--ground);
  border-bottom:2px solid transparent;
  transition:border-color .25s var(--ease-out);
}
.masthead.is-stuck{border-bottom-color:var(--ink)}
.masthead--ruled{border-bottom-color:var(--ink)}

.masthead__in{display:flex;align-items:center;justify-content:space-between;
  gap:var(--gap);padding-block:calc(var(--mod) * .7)}

.nav{display:flex;gap:var(--gap-lg);align-items:center}
.nav a{font-size:var(--fs-control);text-decoration:none;
  font-variation-settings:'wdth' 100,'wght' 500;
  padding-block:12px;position:relative;min-height:44px;
  display:inline-flex;align-items:center}

/* the underline draws from the left, it does not fade in */
.nav a:not(.btn)::after{
  content:'';position:absolute;left:0;right:0;bottom:8px;height:2px;background:currentColor;
  transform:scaleX(0);transform-origin:left;transition:transform .2s var(--ease-out);
}
@media (hover:hover) and (pointer:fine){
  .nav a:not(.btn):hover::after{transform:scaleX(1)}
}

/* 🔑 the element is REQUIRED in this selector. A bare .btn at
   (0,1,0) loses to .nav a{padding-block:12px} at (0,1,1) and the
   button silently drops its vertical padding. */
.nav a.btn{padding:11px 18px;font-variation-settings:'wdth' 100,'wght' 700}

/* 🔑 the flow pages carry NO action in the masthead, so the row's height is the
   text links' own. padding-block:12px above exists to keep a link's box tall
   beside the homepage's outline button, where the button sets the row height
   anyway; with no button it made /contact's header 46px instead of the
   approved design's 44px and pushed every pixel below it down by 2
   (redesign-contact.dc.html:97-100, which sets min-height and no padding).
   min-height:44px still owns the tap target, and the underline's bottom:8px is
   measured from the same box the wireframe measures it from. */
.masthead--ruled .nav a:not(.btn){padding-block:0}

@media (prefers-reduced-motion:reduce){
  .nav a:not(.btn)::after{transition:none}
}

/* one row: the mark and the action. The text links live in the footer,
   so nothing is lost and there is no drawer and no hamburger. */
@media (max-width:640px){
  .nav a:not(.btn){display:none}
  /* 🔑 10px, the phone source's own value, not --mod * .7. At 390 the module
     is 14px, so .7 of it is 9.8 — and the masthead is the FIRST box on the
     page, so its 0.4px lands under everything below it. Every line box down
     the page then rounds from a different fractional origin than the design
     does, which is why a page that matched the source rule for rule still
     drifted a pixel at a time. Fractional height here is not a rounding
     detail, it is the page's origin. */
  .masthead__in{padding-block:10px}
}

/* ==== design-system/components/bar.css ==== */
/* ============================================================
   bar — the signature device

   Two colour fields laid across a headline. Where they cross,
   mix-blend-mode:multiply produces --overlap. The overlap is
   computed by the browser, never authored as a third colour.

   <h1 class="display">
     <span class="hl">Know what you're <span class="bar bar--product">scaling</span></span>
     <span class="hl hl--2">before you <span class="bar bar--growth">scale</span> it</span>
   </h1>

   🔴 NEVER put isolation:isolate on .bar or an ancestor between two
   crossing bars. The multiply needs a SHARED stacking context;
   isolating kills the device outright.

   Overshoot Rule: the field overshoots the word by .55 of a module each
   side (with matching negative margin, so the text line does not shift)
   and clips to -.12em of the line-box. Field height is
   line-height + 2 x inset — at .display's line-height 1.0 the field is
   short and the crossing is deep. The two values move together or not
   at all.
   ============================================================ */

.bar{position:relative;display:inline-block;
  padding-inline:calc(var(--mod) * .55);margin-inline:calc(var(--mod) * -.55)}
.bar::before{content:'';position:absolute;inset:-.12em 0;z-index:-1;mix-blend-mode:multiply}

.bar--product{color:var(--white)}      /* 5.91:1 on --product */
.bar--product::before{background:var(--product)}
.bar--growth{color:var(--ink)}         /* 5.85:1 on --growth. never white */
.bar--growth::before{background:var(--growth)}

/* headline lines. text-wrap:balance so a wrapped line never strands
   one word ("it") on its own. */
.hl{display:block;text-wrap:balance}

/* --2 is the declared step, set from the copy rather than from habit:
   9 modules puts the bar on "scale" in the same column as the bar on
   "scaling" above it, so the two actually multiply. A new headline gets
   the step re-set against its own words (Declared-Step Rule). */
.hl--2{margin-left:calc(var(--mod) * 9)}

/* 🔴 RETIRED, kept on record only: the through-letterform bar. It is the
   Sep 17 pair-thesis wireframe's device — a .17em orange rule driven
   through the one headline line that carries no field. Centred on the
   line it read as a strikethrough; the Sep 18 homepage carried it and the
   rebuild removed it. Do not reach for it on new work.
   Bleed-Without-Scroll: the parent band MUST carry overflow-x:clip, or
   right:-100vw opens a horizontal scrollbar. */
.hl--3{position:relative;display:inline-block}
.hl--3::before{
  content:'';position:absolute;z-index:-1;left:0;right:-100vw;
  top:50%;height:.17em;transform:translateY(-50%);
  background:var(--growth);mix-blend-mode:multiply;
}

/* the hero fields arriving — moment 1 of 3. The rest state above IS the
   final state, and the animation is gated on .js and on no-preference
   with animation-fill-mode:backwards, so no-JS, reduced motion and a
   failed animation all read the finished headline. */
@media (prefers-reduced-motion:no-preference){
  .js .bar::before{animation:wipe-l .7s var(--ease-out) backwards}
  .js .bar--product::before{animation-delay:.15s}
  .js .bar--product{animation:ink-then-white .55s steps(1,end) .15s backwards}
  .js .bar--growth::before{animation-delay:.4s}

  /* the flow pages carry ONE bar, with nothing to stagger against, so it does
     not take the homepage's sequence: it starts at .1s and steps its colour
     over .6s (redesign-contact.dc.html:120-121, redesign-booking.dc.html:
     120-121). Without this, /booking's bar--growth would wait .4s for a
     crossing that is not there. */
  .js .page--flow .bar::before{animation-delay:.1s}
  .js .page--flow .bar--product{animation:ink-then-white .6s steps(1,end) .1s backwards}
}

@media (max-width:900px){.hl--2{margin-left:calc(var(--mod) * 2)}}
@media (max-width:640px){.hl--2{margin-left:0}}

/* D17. The phone source measures the field's inset in EM, not in modules:
   .35em of the headline's own 42.9px is 15px a side, where --mod * .55 is
   7.7px at this width. Half the field. The bar is the page's signature and
   it sits on the largest type on the screen, so the inset has to scale with
   that type rather than with the page's spacing module — which is exactly
   what the phone source says and what the desktop's wider module happens to
   approximate at 1440. Measured: 15px a side, 30px on the field's width. */
@media (max-width:640px){
  .bar{padding-inline:.35em;margin-inline:-.35em}
}

/* ==== design-system/components/beliefs.css ==== */
/* ============================================================
   beliefs — the ruled row turned on its side

   <div class="beliefs">
     <div class="belief reveal" style="--d:0ms">
       <h3>Scaling multiplies whatever is underneath</h3>
       <p>…</p>
     </div>  x3
   </div>

   Three columns, each opened by a 2px ink rule rather than by a label
   above it (Caps-Only-Wordmark bans the eyebrow). The heading is the
   Phase role, capped at 22ch and balanced; one --ink-2 paragraph at
   40ch follows. One column per belief, collapsing to one at 900px.
   ============================================================ */

.beliefs{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--gutter);
  margin-top:calc(var(--mod) * 3)}

.belief{border-top:2px solid var(--ink);padding-top:var(--gap)}
.belief h3{font-variation-settings:'wdth' 108,'wght' 780;font-size:var(--fs-phase);
  letter-spacing:-.025em;line-height:1.15;margin:0;max-width:22ch;text-wrap:balance}
.belief p{margin:calc(var(--mod) * .6) 0 0;color:var(--ink-2);max-width:40ch}

@media (max-width:1100px){.beliefs{gap:var(--gap-lg)}}
@media (max-width:900px){.beliefs{grid-template-columns:1fr}}

/* D17. The 22ch heading cap is ~250px on a 390px screen, so "Get your
   product off the ground" wrapped to two lines where the phone source
   sets it on one — and every belief below it moved down with it. The
   phone's column is the measure; see base.css. */
@media (max-width:640px){
  .beliefs{gap:28px;margin-top:44px}
  .belief h3,.belief p{max-width:none}
  .belief p{margin-top:10px}
}

/* ==== design-system/components/button.css ==== */
/* ============================================================
   button — one family, two fills, four sizes

   .btn                base; inherits nothing, safe on any ground
   .btn--solid         orange field, ink label. The page's primary.
   .btn--outline       2px ink rule, transparent. Fills ink on hover,
                       and flips to WHITE-on-fill when the row or band
                       underneath it fills with colour.
   .btn--ghost         the hero's SECOND action. Same geometry as
                       .btn--solid, an ink rule instead of the orange
                       field, so the two sit on one line at one height.
   .btn--lg            the closing action (see close.css for --ink)
   .btn--field         a form's submit: 52px, matching the field height
   .btn--sm            inline commitment chip inside a listrow

   D7: the pages use these names. The wireframes' .action / .action--ink
   / .action--ghost / .nav__cta / .svc__shape all resolve here — see
   rename-map.json.

   Never-White-On-Orange: white on --growth is 3.10:1 and fails.
   A solid orange button is ALWAYS ink-labelled (5.85:1).
   Flat-By-Construction: hover swaps the whole field over .18s and
   changes colour only. The one transform is the .97 press.
   ============================================================ */

.btn{
  display:inline-flex;align-items:center;
  min-height:44px;padding:16px 26px;
  border:0;background:transparent;color:inherit;
  font:inherit;font-variation-settings:'wdth' 104,'wght' 800;
  font-size:var(--fs-action);letter-spacing:-.01em;
  text-decoration:none;white-space:nowrap;cursor:pointer;
  transition:background-color .18s var(--ease-out),
             color .18s var(--ease-out),
             border-color .18s var(--ease-out),
             transform .12s var(--ease-out);
}
.btn:active{transform:scale(.97)}

.btn--solid{background:var(--growth);color:var(--ink)}
@media (hover:hover) and (pointer:fine){
  .btn--solid:hover{background:var(--ink);color:var(--white)}
}

/* on a dark band the hover inverts the other way, so the button
   never disappears into the ground it sits on */
.btn--solid.on-dark:hover{background:var(--white);color:var(--ink)}

.btn--outline{
  border:2px solid var(--ink);color:var(--ink);background:transparent;
  font-variation-settings:'wdth' 100,'wght' 700;
}
@media (hover:hover) and (pointer:fine){
  .btn--outline:hover{background:var(--ink);color:var(--white)}
}

/* 🔑 an INSET SHADOW, not a border, and this is the whole point of the
   modifier. .btn--outline's 2px border adds 4px to the box, and the
   hero sets this button beside .btn--solid, which has none — a border
   here would stand the secondary action 4px taller than the primary on
   the same line. An inset shadow draws the same rule inside the box. */
.btn--ghost{background:transparent;color:var(--ink);box-shadow:inset 0 0 0 2px var(--ink)}
@media (hover:hover) and (pointer:fine){
  .btn--ghost:hover{background:var(--ink);color:var(--white)}
}

.btn--lg{font-size:clamp(1.05rem,1.4vw,1.28rem);padding:20px 34px}
.btn--sm{font-size:var(--fs-small);padding:9px 15px;min-height:38px}

/* the form submit sits in a column of 52px fields and matches them.
   gap:.7em because the label is wrapped in a <span> the handler swaps
   to "Sending…" — the gap holds for any label length. */
.btn--field{padding:17px 28px;min-height:52px;gap:.7em}

/* in flight: the handler disables the button and swaps the label. Ink
   field, white label (18.11:1), no press transform while it waits. */
.btn[disabled]{background:var(--ink);color:var(--white);cursor:progress;transform:none}

@media (prefers-reduced-motion:reduce){
  .btn:active{transform:none}
}

@media (max-width:640px){
  /* a 52px submit goes full width rather than leaving a stranded
     button beside a wrapped note */
  .btn--field{width:100%;justify-content:center}
}

/* ==== design-system/components/close.css ==== */
/* ============================================================
   close — the page's last boundary (signature)

   <section class="close" id="contact">
     <span class="close__field" aria-hidden="true"></span>
     <div class="sheet">
       <h2 class="mega">Tell us what you're scaling</h2>
       <div class="close__act">
         <a class="btn btn--ink btn--lg" href="/contact">Book a call</a>
         <p class="close__note">…</p>
       </div>
     </div>
   </section>

   The page ends on its largest field, not on a second dark band. A blue
   field min(21%, 300px) wide, anchored to the right gutter, starts 2.2
   modules ABOVE the band and runs 62% down it, multiplied: it reads
   blue against the ground above and --overlap over the orange below.
   It takes no pointer events, and it is removed at 900px and below.

   Never-White-On-Orange, and no orange-on-orange either: the action
   inverts to an ink field with a white label (18.11:1) and its note is
   --on-growth (4.89:1). The headline is capped at 16ch so inked type
   stays where the blue field cannot reach it.

   The band must carry overflow-x:clip (Bleed-Without-Scroll).
   ============================================================ */

.close{position:relative;background:var(--growth);color:var(--ink);
  padding-block:calc(var(--mod) * 6) calc(var(--mod) * 5);overflow-x:clip}
/* the one place Mega is raised: equal to the thesis, never larger */
.close .mega{max-width:16ch;font-size:clamp(2.4rem,6.1vw,6rem)}

.close__field{position:absolute;right:var(--gutter);top:calc(var(--mod) * -2.2);
  width:min(21%,300px);height:calc(62% + var(--mod) * 2.2);
  background:var(--product);mix-blend-mode:multiply;pointer-events:none}

.close__act{margin-top:calc(var(--mod) * 2.2);display:flex;flex-wrap:wrap;
  gap:var(--gap-lg);align-items:center;position:relative}
.close__note{color:var(--on-growth);font-size:var(--fs-control);max-width:36ch;margin:0}

/* the button modifier that exists only for this band: an orange field
   cannot carry an orange or a white button, so it carries an ink one. */
.btn--ink{background:var(--ink);color:var(--white)}
@media (hover:hover) and (pointer:fine){
  .btn--ink:hover{background:var(--white);color:var(--ink)}
}

@media (max-width:900px){.close__field{display:none}}

/* D17. The phone sets the close LARGER than --fs-mega, not smaller: it
   is the last screen and it is the only thing on it. The 16ch cap is
   dropped with it — at 390 a 16ch measure breaks "Let's Start Growing"
   onto three lines where the phone source sets two — and the action
   goes full width because there is no note beside it any more. */
@media (max-width:640px){
  .close{padding-block:72px 64px}
  .close .mega{max-width:none;font-size:clamp(2.6rem,12vw,3.6rem)}
  .close__act{display:grid;margin-top:32px}
  .close__act .btn{justify-content:center}
  /* .btn--lg's clamp floors at 1.05rem here; the phone source sets its
     .action--ink at 1.1rem. Scoped to --ink, which is this band's action and
     nothing else. */
  .btn--ink{font-size:1.1rem}
}

/* ==== design-system/components/duo.css ==== */
/* ============================================================
   duo — the operator duo (signature)

   The two operators AS the two fields, in the right half of the hero.

   <div class="duo">
     <span class="duo__f duo__f--p" aria-hidden="true"></span>
     <span class="duo__f duo__f--g" aria-hidden="true"></span>
     <img class="duo__ph duo__ph--a" src="…" alt="Andrea Perez" width="800" height="800">
     <img class="duo__ph duo__ph--m" src="…" alt="Matias Honorato" width="800" height="800">
     <div class="duo__who duo__who--a"><b>Andrea Perez</b>Product &amp; Positioning
       <span class="duo__ex"><img …><i aria-hidden="true">|</i><img …></span></div>
     <div class="duo__who duo__who--m"><b>Matias Honorato</b>Growth &amp; GTM
       <span class="duo__ex">…</span></div>
   </div>

   A 1 / .92 box with its OWN stacking context, so the two fields
   multiply with each other and with nothing behind them. The blue field
   holds the left 60%, the orange field the right 60%, and the 20% they
   share paints itself --overlap.

   🔑 A slab carries its own field colour, so the label is legible even
   if the field behind it never paints (Legible-At-Rest). The growth slab
   is anchored at left:60%, exactly where the blue field ends, so inked
   type can never land on the crossing
   (Nothing-Inked-On-Overlap is positional as well as tonal).

   The hero band must carry overflow-x:clip (see hero.css).
   ============================================================ */

.duo{position:relative;aspect-ratio:1/.92;isolation:isolate}

.duo__f{position:absolute;mix-blend-mode:multiply}
.duo__f--p{left:0;top:14%;width:60%;height:62%;background:var(--product)}
.duo__f--g{right:0;top:34%;width:60%;height:62%;background:var(--growth)}

/* 🔑 height:auto is load-bearing: the width/height attributes the markup
   needs for CLS map to a presentational height that otherwise beats
   aspect-ratio and squares the portrait. */
.duo__ph{position:absolute;width:36%;height:auto;aspect-ratio:4/5;object-fit:cover;display:block;
  filter:grayscale(1) contrast(1.06);background:var(--ink)}
.duo__ph--a{left:4%;top:0}
.duo__ph--m{right:4%;bottom:4%;aspect-ratio:1/1}   /* square, anchored to the foot */

/* a name slab, not a caption underneath */
.duo__who{position:absolute;margin:0;line-height:1.2;font-size:var(--fs-small);padding:7px 11px 8px}
.duo__who b{display:block;font-variation-settings:'wdth' 104,'wght' 800;
  font-size:var(--fs-control);letter-spacing:-.02em}
.duo__who--a{left:calc(4% - 11px);top:52%;color:var(--white);background:var(--product)}  /* 5.91:1 */
.duo__who--m{left:60%;bottom:calc(4% + 36% / .92 + 6px);white-space:nowrap;
  color:var(--ink);background:var(--growth)}                                             /* 5.85:1 */

/* where they learned it: prior-company wordmarks under the role, mono in
   the slab's own ink (white on product, ink on growth). The slab sits
   6px above Matias's square portrait: 36% of the duo's width is
   36% / .92 of its height. */
.duo__ex{display:flex;align-items:center;gap:6px;margin-top:6px;padding-top:6px;
  border-top:1px solid color-mix(in srgb,currentColor 35%,transparent);
  font-size:var(--fs-micro);opacity:.85;white-space:nowrap}
.duo__ex img{height:11px;width:auto;display:block;filter:brightness(0)}
.duo__who--a .duo__ex img{filter:brightness(0) invert(1)}
.duo__ex i{font-style:normal;opacity:.5}
.duo__who--m .duo__ex{gap:4px}
.duo__who--m .duo__ex img{height:9px}

/* moment 1 of 3, continued from bar.css: the fields wipe in from their
   own side, the portraits rise .9 of a module, the slabs fade in last.
   Geometry and opacity only — a field's paint is never animated. */
@media (prefers-reduced-motion:no-preference){
  .js .duo__f--p{animation:wipe-l .8s var(--ease-out) .35s backwards}
  .js .duo__f--g{animation:wipe-r .8s var(--ease-out) .55s backwards}
  .js .duo__ph--a{animation:rise .6s var(--ease-out) .25s backwards}
  .js .duo__ph--m{animation:rise .6s var(--ease-out) .45s backwards}
  .js .duo__who{animation:fade .4s var(--ease-out) 1.15s backwards}
}

@media (max-width:900px){.duo{max-width:520px}}
@media (max-width:640px){
  /* the slab drops a step so two names still fit beside each other */
  .duo__who{padding:5px 7px 6px;font-size:var(--fs-micro)}
  .duo__who b{font-size:var(--fs-small)}
  .duo__who--a{left:calc(4% - 7px)}
}

/* D17: the phone design has NO portraits in the hero. The composition
   is two overlapping fields with two faces in them, and at 350px of
   content width the faces are 126px wide and the crossing is a smear,
   so the phone source drops it and puts the operators' portraits in the
   team section instead, at 160px square (.who__ph, ledger.css).
   The markup stays — this is one page, and above 640 the duo is the
   hero's right-hand half. */
@media (max-width:640px){.duo{display:none}}

/* ==== design-system/components/faq.css ==== */
/* ============================================================
   faq — the disclosure list

   <section class="band band--tint" id="faq" aria-labelledby="faq-title">
     <div class="sheet faq">
       <h2 class="mega faq__h" id="faq-title">Frequently asked questions</h2>
       <div class="faq__list">
         <details class="faq__d">
           <summary class="faq__q"><h3 class="faq__qt">…</h3><span class="faq__caret" aria-hidden="true"></span></summary>
           <div class="faq__a"><p>…</p></div>
         </details>
       </div>
     </div>
   </section>

   Sep 23, 2026 port of the owner's Claude Design FAQ
   (redesign-homepage.dc.html + redesign-homepage-mobile.dc.html). It
   replaces the Sep 17 plus/minus disclosure, which D18 had taken off the page.

   Native <details>/<summary>: keyboard operable without a line of script,
   and the answer text stays in the DOM for a crawler whether or not the
   item is open. Each question is an h3 inside <summary> so it stays a real
   heading for crawlers and answer engines; .faq__qt strips the heading's
   UA styling so the summary's type wins.

   Desktop: sticky title in a 5fr column, the list in 7fr. At 860px the grid
   collapses and the title stops sticking.

   The FAQPage JSON-LD in index.html must stay equal in text to what is
   visible here (verify_site.py faq-drift, scoped to .faq__d).
   ============================================================ */

.faq{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:var(--gap-lg) calc(var(--mod) * 3);align-items:start}
.faq__h{max-width:11ch;position:sticky;top:calc(var(--mod) * 6)}
.faq__list{border-top:2px solid var(--ink)}
.faq__d{border-bottom:1px solid var(--hair)}
.faq__q{display:flex;align-items:center;justify-content:space-between;gap:var(--gap);padding-block:20px;min-height:44px;cursor:pointer;list-style:none;
  font-variation-settings:'wdth' 106,'wght' 760;font-size:var(--fs-phase);letter-spacing:-.025em;line-height:1.2;-webkit-tap-highlight-color:transparent}
.faq__q::-webkit-details-marker{display:none}
.faq__q::marker{content:""}
.faq__qt{font:inherit;letter-spacing:inherit;margin:0;text-wrap:balance}
.faq__caret{flex:none;width:.5em;height:.5em;border-right:2.5px solid var(--ink);border-bottom:2.5px solid var(--ink);
  transform:translateY(-.18em) rotate(45deg);transition:transform .22s var(--ease-out);opacity:.6}
.faq__d[open] .faq__caret{transform:translateY(.1em) rotate(225deg);opacity:1}
.faq__d[open] .faq__q{color:var(--ink)}
.faq__a{padding:0 0 24px;color:var(--ink-2);font-size:var(--fs-list);line-height:1.5;max-width:62ch}
.faq__a p{margin:0}
.faq__a p + .faq__modes{margin-top:12px}
.faq__modes{list-style:none;margin:0;padding:0;display:grid;gap:12px}
.faq__modes li{padding-left:14px;position:relative}
.faq__modes li::before{content:'';position:absolute;left:0;top:.55em;width:6px;height:6px;background:var(--growth)}
.faq__modes li:nth-child(2)::before{background:var(--product)}
.faq__modes li:nth-child(3)::before{background:var(--overlap)}
.faq__modes strong{color:var(--ink);font-variation-settings:'wdth' 100,'wght' 700;font-weight:inherit}

@media (hover:hover) and (pointer:fine){
  .faq__q:hover .faq__caret{opacity:1}
  .faq__q:hover .faq__qt{text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:.2em}
}
.faq__d[open] .faq__a{animation:ledNote .24s var(--ease-out) both}

@media (max-width:860px){.faq{grid-template-columns:1fr}.faq__h{position:static}}
@media (max-width:640px){.faq{gap:28px}.faq__h{max-width:none}}
@media (prefers-reduced-motion:reduce){
  .faq__caret{transition:none}
  .faq__d[open] .faq__a{animation:none}
}

/* ==== design-system/components/fit.css ==== */
/* ============================================================
   fit — the two fit cards

   <div class="fitgrid">
     <div class="fitcard fitcard--yes reveal">
       <span class="fitcard__rule" aria-hidden="true"></span>
       <h3 class="fitcard__k">A good fit</h3>
       <p class="fitcard__sub">…one qualifying line…</p>
       <ul><li><span class="tick tick--on-product" aria-hidden="true">…</span>
               <span>Pre-seed to Series B, with early traction</span></li></ul>
     </div>
     <div class="fitcard fitcard--no reveal" style="--d:80ms">…tick--muted…</div>
   </div>

   One pair, and the ASYMMETRY between them is the content: the good-fit
   card is a blue field with white type, the not-a-fit card is the
   outline panel, deliberately receded — white fill, 2px --ink-2 border,
   --ink-2 type (6.38:1 on white). Do not make them match.

   Each card opens with a 3px rule, then a key at the Row role and one
   qualifying line, then list rows at --fs-list led by a tick or a cross
   in a 26px square (tick.css).

   Ink on product blue is 3.06:1 and passes as large text only, which is
   why the key is white here and every list row is white too.
   ============================================================ */

.fit__lede{font-size:clamp(1.02rem,1.2vw,1.18rem);line-height:1.45;max-width:52ch;
  margin:var(--gap-lg) 0 0;color:var(--ink-2)}

.fitgrid{display:grid;grid-template-columns:1fr 1fr;gap:var(--gap-lg);
  margin-top:calc(var(--mod) * 2.4)}

.fitcard{padding:calc(var(--mod) * 1.8);display:flex;flex-direction:column}
.fitcard__rule{height:3px;margin-bottom:calc(var(--mod) * 1.5)}
.fitcard__k{font-variation-settings:'wdth' 106,'wght' 800;font-size:var(--fs-row);
  letter-spacing:-.03em;line-height:1;margin:0}
.fitcard__sub{font-size:var(--fs-control);margin:calc(var(--mod) * .55) 0 0;max-width:34ch}
.fitcard ul{list-style:none;margin:calc(var(--mod) * 1.6) 0 0;padding:0}
.fitcard li{display:grid;grid-template-columns:auto 1fr;gap:calc(var(--mod) * .8);
  align-items:start;padding-block:calc(var(--mod) * .75);font-size:var(--fs-list);line-height:1.35}

.fitcard--yes{background:var(--product);color:var(--white)}
.fitcard--yes .fitcard__rule{background:var(--white)}
.fitcard--yes .fitcard__sub{color:var(--on-product)}
.fitcard--yes li{border-top:1px solid rgba(255,255,255,.34)}
.fitcard--yes li:first-child{border-top:0;padding-top:0}

.fitcard--no{background:var(--white);color:var(--ink-2);border:2px solid var(--ink-2)}
.fitcard--no .fitcard__rule{background:var(--hair)}
.fitcard--no .fitcard__k,.fitcard--no .fitcard__sub{color:var(--ink-2)}
.fitcard--no li{border-top:1px solid var(--hair)}
.fitcard--no li:first-child{border-top:0;padding-top:0}

@media (max-width:900px){
  .fitgrid{grid-template-columns:1fr}
  .fitcard{padding:var(--pad-cell)}
}

/* D17. --mod is 14px at 390 (base.css says why it is not restated), so
   the module multiples above land 1-3px under the phone source's
   authored values. Small on their own; they stack down the card, and
   two cards of six rows put the close ~20px out of register. */
@media (max-width:640px){
  .fit__lede{font-size:1.05rem}
  .fitgrid{margin-top:36px}
  .fitcard{padding:24px}
  .fitcard__rule{margin-bottom:22px}
  .fitcard__sub{margin-top:9px}
  .fitcard ul{margin-top:24px}
  .fitcard li{gap:12px;padding-block:12px}
  .fit__lede{max-width:none}
  /* 34ch is ~250px at --fs-control on the phone: the qualifying line
     wrapped a word early in both cards */
  .fitcard__sub{max-width:none}
}

/* ==== design-system/components/flow.css ==== */
/* ============================================================
   flow — the shell of the two task pages (/contact, /booking)

   <body class="page--flow">
     …masthead…
     <main id="main-content">
       <section class="flow">
         <div class="sheet">
           <h1 class="display display--alt">…</h1>
           <p class="lede lede--task">…</p>
           <ol class="steps">…</ol>
           <div class="flow__grid">
             <div class="panel">…</div>
             <aside class="next">…</aside>
           </div>
         </div>
       </section>
     </main>
     …footer…
   </body>

   .page--flow is the sticky-footer shell, and it is a MODIFIER rather
   than a bare body rule because one bundle serves every page: turning
   <body> into a flex column globally would change the homepage too.

   A 7fr / 5fr grid — the work on the left, what happens next on the
   right — collapsing to one column at 900px. overflow-x:clip because
   .next__orange reaches past its column's right edge.
   ============================================================ */

body.page--flow{min-height:100vh;display:flex;flex-direction:column}
body.page--flow main{flex:1}

.flow{padding-block:calc(var(--mod) * 3.2) var(--pad-band);overflow-x:clip}
.flow__grid{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:var(--gutter);
  align-items:start;margin-top:calc(var(--mod) * 2.6)}

@media (max-width:900px){.flow__grid{grid-template-columns:1fr}}

/* ==== design-system/components/footer.css ==== */
/* ============================================================
   footer — the overlap field at the foot of the page

   Two variants, one class:

   .foot__cols  the homepage footer. 1.3fr / 1fr / 1fr — brand and one
                line at left, then two link columns. It also carries the
                masthead's text links, which is what lets the masthead
                drop them at 640px without losing anything.

   .foot__in    the flow footer (/contact, /booking). One compact row:
                the same legal line and one mailto.

   Nothing-Inked-On-Overlap: ink is 1.05:1 here. Type is white or
   --on-overlap (13.90:1), the heading white, and the legal divider is
   --rule-dark (growth orange, 5.59:1 — the one ground where orange is
   allowed to be a thin line). Blue never appears on this field.

   The brand lockup inverts by colour, not by a second asset:
   .foot__brand .mark__glyph flips ink and white.

   .foot__word is the wordmark AS GROUND: set to the full sheet width at
   12% white, sitting low enough that its baseline runs under the legal
   line. It is decoration, so it is aria-hidden in the markup, takes no
   pointer events and cannot be selected — the readable wordmark is the
   lockup above it. The negative bottom margin is what pulls the legal
   rule up into it; .foot carries overflow:clip so the descender cannot
   open a scrollbar (Bleed-Without-Scroll).
   ============================================================ */

.foot{background:var(--overlap);color:var(--on-overlap);position:relative;overflow:clip}

.foot__word{display:block;width:100%;
  margin:calc(var(--mod) * 2.4) 0 calc(var(--mod) * -1.2);
  font-variation-settings:'wdth' 100,'wght' 800;font-size:clamp(4rem,17.2vw,20rem);
  line-height:.78;letter-spacing:-.04em;color:rgba(255,255,255,.12);
  user-select:none;pointer-events:none;text-align:center}

.foot__cols{display:grid;grid-template-columns:1.3fr 1fr 1fr;gap:var(--gap-lg);
  padding-block:calc(var(--mod) * 2.2)}
.foot__brand{display:flex;align-items:center;gap:calc(var(--mod) * .5)}
.foot__brand .mark__glyph{background:var(--white);color:var(--ink)}
.foot__brand .mark__word{color:var(--white)}
.foot__tag{color:var(--on-overlap);font-size:var(--fs-small);margin:var(--gap) 0 0;max-width:30ch}

.foot h3{font-variation-settings:'wdth' 100,'wght' 700;font-size:var(--fs-control);
  letter-spacing:.02em;color:var(--white);margin:0 0 calc(var(--mod) * .3)}
.foot ul{list-style:none;margin:0;padding:0}
.foot a{font-size:var(--fs-control);text-decoration:none;color:var(--on-overlap);
  position:relative;display:inline-flex;align-items:center;min-height:44px}
.foot a::after{content:'';position:absolute;left:0;right:0;bottom:10px;height:1px;
  background:currentColor;
  transform:scaleX(0);transform-origin:left;transition:transform .2s var(--ease-out)}
@media (hover:hover) and (pointer:fine){
  .foot a:hover{color:var(--white)}
  .foot a:hover::after{transform:scaleX(1)}
}
@media (prefers-reduced-motion:reduce){.foot a::after{transition:none}}

.foot__legal{display:flex;flex-wrap:wrap;gap:var(--gap);justify-content:space-between;
  border-top:1px solid var(--rule-dark);padding-block:var(--gap);
  font-size:var(--fs-micro);color:var(--on-overlap)}

/* the flow pages' one-row footer */
.foot__in{display:flex;flex-wrap:wrap;gap:var(--gap) var(--gap-lg);
  justify-content:space-between;align-items:center;
  padding-block:calc(var(--mod) * 1.2);font-size:var(--fs-micro)}

@media (max-width:900px){
  .foot__cols{grid-template-columns:1fr 1fr;row-gap:calc(var(--mod) * 2)}
  .foot__brandcol{grid-column:1/-1}
}
@media (max-width:640px){
  .foot__cols{grid-template-columns:1fr 1fr;gap:32px var(--gap);padding-block:40px}
  /* D17: on the phone the brand block spans and the two link columns
     share the row, rather than three full-width stacks. */
  .foot__brandcol{grid-column:1/-1}
  /* 17.2vw of 390 is 67px; the phone source caps the wordmark at 20vw
     and 86px so it fills the column instead of sitting in the middle
     of it. */
  .foot__word{margin:24px 0 -12px;font-size:min(20vw,86px)}
  .foot__legal{gap:8px var(--gap);
    padding-block:var(--gap) calc(var(--gap) + env(safe-area-inset-bottom))}
  .foot__tag{max-width:none}
  /* --mod * .3 is 4.2px here; the phone source writes 4 */
  .foot h3{margin-bottom:4px}
}

/* ==== design-system/components/form.css ==== */
/* ============================================================
   form — the contact form's fields, errors and failure

   <form class="form" id="contactForm">
     <div class="form__row">
       <div class="field">
         <label for="firstName">First name</label>
         <input type="text" id="firstName" name="firstName" required>
         <span class="field__err">Add your first name.</span>
       </div>  x2
     </div>
     <p class="form__fail" id="formFail" role="alert">That didn't send. …</p>
     <div class="form__foot">
       <button type="submit" class="btn btn--solid btn--field"><span>Send and pick a time</span></button>
       <p class="form__micro">…</p>
     </div>
   </form>

   Square, ink-ruled fields on white, 52px tall, inside .panel.
   🔑 font-size:max(16px,1rem) is not cosmetic: iOS zooms the whole page
   when a focused field is under 16px.
   🔑 appearance:none plus a data-URI chevron for the select — an
   external SVG would be a second request, and the same file-protocol
   trap as the logomark mask.

   Errors NAME the problem and appear only after the person has tried
   the field: :user-invalid, not :invalid, so a form is not red before
   it has been touched. The message is --accent-text (5.68:1 on white),
   the only orange that passes as text.

   .form__fail is the fallback both endpoints failing: a bordered
   --accent-text block with role="alert", shown by .is-on. The submit
   is re-enabled with it, so the person can try again.

   The button is button.css (.btn--field is the 52px size). The field
   names, the select's option VALUES and the endpoints are the live
   page's contract — US-006 guards them, this file only styles them.
   ============================================================ */

.form{display:grid;gap:calc(var(--mod) * 1.3)}
.form__row{display:grid;grid-template-columns:1fr 1fr;gap:var(--gap)}

.field{display:grid;gap:8px;min-width:0}
.field label{font-variation-settings:'wdth' 100,'wght' 700;font-size:var(--fs-control)}
/* the group label of the services field: a span, because a <label for> points
   at ONE control and that field holds three. Same weight and size as a label. */
.field__legend{font-variation-settings:'wdth' 100,'wght' 700;font-size:var(--fs-control)}
.field input,.field select,.field textarea{
  font:inherit;font-size:max(16px,1rem);
  color:var(--ink);background:var(--white);
  border:2px solid var(--ink);border-radius:0;padding:13px 14px;min-height:52px;width:100%;
  appearance:none;-webkit-appearance:none;
  transition:background-color .15s var(--ease-out);
}
.field textarea{min-height:10em;resize:vertical;line-height:1.5;display:block}
.field ::placeholder{color:var(--ink-2);opacity:1}          /* 6.38:1 on white */
.field select{padding-right:44px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2314161A' stroke-width='2.4' stroke-linecap='square'%3E%3Cpath d='M5 9l7 7 7-7'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 14px center;background-size:16px}
.field select:invalid{color:var(--ink-2)}                   /* the disabled placeholder option */
@media (hover:hover) and (pointer:fine){
  .field input:hover,.field select:hover,.field textarea:hover{background-color:var(--ground)}
}
.field input:focus,.field select:focus,.field textarea:focus{background-color:var(--white)}

/* ------------------------------------------------------------
   service interest (US-012/D21): a real dropdown holding checkboxes,
   styled like the select beside it.

     <div class="field">
       <span class="field__legend" id="svcLabel">What are you interested in?</span>
       <div class="multi" id="svcMulti">
         <button type="button" class="multi__btn" aria-expanded="false">
           <span class="multi__ph" id="svcSummary">Select one or more</span>
         </button>
         <div class="multi__panel" role="group" aria-labelledby="svcLabel">
           <label class="pick" for="svc1"><input id="svc1" type="checkbox"
             name="services" value="One-off Project support"><span>…</span></label>  x3
         </div>
       </div>
     </div>

   🔑 A <select multiple> is the native control and is NOT used: it renders as
   a scrolling list box, needs cmd-click to take a second value, and cannot be
   made to look like the single-select next to it. The button + panel is the
   shape the owner approved in Claude Design.

   .field__legend is the label of a GROUP, so it is a span, not a <label for>:
   a label points at one control and this field has three. The panel carries
   role="group" + aria-labelledby, which is what names the set.

   .multi__ph is the placeholder STATE of the summary, not a second element:
   the script drops the class as soon as a box is checked, so the summary goes
   from --ink-2 placeholder to --ink content in place.

   .pick inverts to the ink field when its box is checked (:has), which is the
   same "the chosen row fills" move the service rows make on the homepage. The
   check itself is --growth on ink, drawn as a data-URI so the panel costs no
   extra request (the same file-protocol trap as the select chevron).
   ------------------------------------------------------------ */
.multi{position:relative;min-width:0}
.multi__btn{width:100%;text-align:left;font:inherit;font-size:max(16px,1rem);
  color:var(--ink);background:var(--white);border:2px solid var(--ink);
  padding:14px 44px 14px 16px;min-height:56px;cursor:pointer;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2314161A' stroke-width='2.4' stroke-linecap='square'%3E%3Cpath d='M5 9l7 7 7-7'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 14px center;background-size:16px}
.multi__btn:focus-visible{outline:2px solid var(--accent-text);outline-offset:2px}
.multi__btn[aria-expanded="true"]{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2314161A' stroke-width='2.4' stroke-linecap='square'%3E%3Cpath d='M19 15l-7-7-7 7'/%3E%3C/svg%3E")}
.multi__ph{color:var(--ink-2)}
.multi__panel{position:absolute;z-index:5;top:calc(100% - 2px);left:0;right:0;
  background:var(--white);border:2px solid var(--ink);display:none}
.multi.is-open .multi__panel{display:block}

.pick{display:flex;align-items:center;gap:12px;min-height:52px;cursor:pointer;
  padding:12px 16px;background:var(--white);color:var(--ink);
  font-size:var(--fs-control);line-height:1.25;font-variation-settings:'wdth' 100,'wght' 400;
  border-top:1px solid var(--hair);
  transition:background-color .16s var(--ease-out), color .16s var(--ease-out)}
.pick:first-child{border-top:0}
.pick input{appearance:none;-webkit-appearance:none;margin:0;
  width:22px;height:22px;min-width:22px;flex:0 0 22px;align-self:center;
  padding:0;min-height:0;box-sizing:border-box;
  border:2px solid var(--ink);background:var(--white);display:block;
  background-repeat:no-repeat;background-position:center;background-size:15px;
  transition:background-color .14s var(--ease-out), border-color .14s var(--ease-out)}
.pick input:checked{background-color:var(--growth);border-color:var(--ink);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2314161A' stroke-width='4' stroke-linecap='square'%3E%3Cpath d='M4 12.5 9.5 18 20 6.5'/%3E%3C/svg%3E")}
.pick:has(input:checked){background:var(--ink);color:var(--white);
  font-variation-settings:'wdth' 100,'wght' 700}
.pick:has(input:focus-visible){outline:2px solid var(--accent-text);outline-offset:-2px}
@media (hover:hover) and (pointer:fine){
  .pick:hover{background:var(--ink);color:var(--white)}
}

.field__err{display:none;font-size:var(--fs-small);color:var(--accent-text);
  font-variation-settings:'wdth' 100,'wght' 700}           /* 5.68:1 on white */
.field:has(:user-invalid) .field__err{display:block}
.field :user-invalid{border-color:var(--accent-text);box-shadow:inset 0 0 0 1px var(--accent-text)}

.form__foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--gap) var(--gap-lg);
  margin-top:calc(var(--mod) * .4)}
.form__micro{font-size:var(--fs-small);color:var(--ink-2);margin:0;max-width:30ch}

.form__fail{display:none;margin:0;padding:14px 16px;border:2px solid var(--accent-text);
  color:var(--accent-text);font-variation-settings:'wdth' 100,'wght' 700;font-size:var(--fs-control)}
.form__fail.is-on{display:block}
.form__fail a{color:inherit}

@media (max-width:640px){.form__row{grid-template-columns:1fr}}

/* ==== design-system/components/hero.css ==== */
/* ============================================================
   hero — the first viewport's composition

   <section class="hero">
     <div class="sheet">
       <h1 class="display">…two .hl lines…</h1>
       <div class="hero__grid">
         <div>
           <p class="lede">…</p>
           <div class="hero__act">
             <a class="btn btn--solid" href="/contact">Book a call</a>
             <a class="btn btn--ghost" href="#process">How we work</a>
           </div>
           <div class="clients">…</div>
         </div>
         <div class="duo">…</div>
       </div>
     </div>
   </section>

   A 6fr / 6fr grid: the argument and the proof on the left, the operator
   duo on the right. It collapses to one column at 900px, where the duo
   is capped at 520px (duo.css).

   overflow-x:clip is REQUIRED and not cosmetic: the headline fields
   overshoot their words and the duo's fields bleed, and without the clip
   a travelling field opens a horizontal scrollbar on the whole page
   (Bleed-Without-Scroll — verify scrollWidth == clientWidth).

   The Sep 22 redesign replaced the --ink-2 note beside the action with
   a SECOND action (.btn--ghost, "How we work" -> #process). There is
   still one primary label: the ghost is an ink rule, not a second
   orange field, so "Book a call" is the only filled button on the
   screen. .hero__micro stays in the system as an available part; this
   page no longer sets one.

   On the phone the two actions stack full width and the portraits are
   gone entirely (.duo is a >640px composition), so the hero is the
   thesis, one line, two buttons and the marquee.
   ============================================================ */

.hero{padding-block:calc(var(--mod) * 3.4) calc(var(--mod) * 4);overflow-x:clip}
.hero__grid{display:grid;grid-template-columns:minmax(0,6fr) minmax(0,6fr);gap:var(--gutter);
  align-items:start;margin-top:calc(var(--mod) * 2.6)}
.hero__act{display:flex;flex-wrap:wrap;align-items:center;gap:var(--gap);
  margin-top:var(--gap-lg)}
.hero__micro{font-size:var(--fs-small);color:var(--ink-2);margin:0;max-width:28ch}

/* 🔴 minmax(0,1fr), never a bare 1fr. `1fr` is minmax(AUTO,1fr), and the
   auto floor is the item's min-content width — which now contains the
   marquee, whose .clients__track is width:max-content (four logo sets,
   ~1800px). The column grew to fit it, so at 390 the hero's whole left
   half was wider than the page: the lede stopped wrapping, both buttons
   ran past the right gutter, and .hero{overflow-x:clip} hid the evidence
   instead of showing it as a scrollbar. Measured against the phone source
   this was 25px of height and the largest single SSIM loss on the page. */
@media (max-width:900px){.hero__grid{grid-template-columns:minmax(0,1fr)}}

@media (max-width:640px){
  .hero{padding-block:44px 56px}
  /* the grid has one child left once .duo is gone, so the 2.6-module top
     margin it carried belongs to .hero__lede instead.
     🔴 BOTH of these belong INSIDE this block and the pair only makes sense
     together. .hero__lede is a phone-source rule (the desktop design has no
     such class: its .lede is margin:0 and .hero__grid supplies the space).
     Declared unscoped it still applied above 640, where .hero__grid's margin
     is ALSO there — so the lede, both actions and the marquee sat 28px low on
     every desktop width. Measured h1-to-lede: 82px against the desktop
     source's 54 at 1440, 64 vs 36 at 900, 65 vs 37 at 641. The 390 render was
     correct throughout, and the 1440 SSIM still passed, because the shift is
     uniform inside one grid column and SSIM scores alignment, not offset. */
  .hero__grid{margin-top:0}
  .hero__lede{margin-top:28px}
  .hero__act{display:grid;gap:12px;margin-top:28px}
  .hero__act .btn{justify-content:center;min-height:52px}
}

/* ==== design-system/components/ledger.css ==== */
/* ============================================================
   ledger — the operator pair and the track record

   <div class="pairgrid">
     <article class="who who--product">
       <div class="who__head">
         <h3 class="who__slab">Andrea Perez</h3>
         <p class="who__role">Product</p>
         <a class="who__li" href="…" aria-label="… on LinkedIn"><svg …></a>
       </div>
       <img class="who__ph" src="…" alt="Andrea Perez" width="160" height="160">
       <p class="who__bio">…</p>
       <ul class="led">
         <li class="led__row reveal" style="--d:0ms">
           <img class="led__logo" src="…" alt="Bynder" width="129" height="28" loading="lazy">
           <details class="led__d">
             <summary class="led__sum"><span class="led__fig num">$0 to $4M ARR</span>
               <span class="led__caret" aria-hidden="true"></span></summary>
             <span class="led__note">Product strategy. …</span>
           </details>
         </li>
       </ul>
     </article>  x2
   </div>

   Two operators side by side. The two ledgers share rows by `subgrid`,
   so both start on one line whatever the bios do — with an
   @supports not fallback to plain blocks.

   Figures-Are-Attributed: every figure is set adjacent to the name and
   prior role it belongs to, and every one is visible AT REST. This
   replaced the Sep 18 hover tooltip, where the page's only hard numbers
   sat behind grey logos. The logo's colour-on-hover is the only thing
   hover does here, and (hover:none) gets it at rest.

   The Sep 22 redesign made each row a native <details>, CLOSED by
   default. The invariant above is unchanged and is the reason the split
   falls where it does: the FIGURE is the <summary> and is always
   visible; only the role note behind it is disclosed. Nothing that
   carries a claim is ever hidden. Native <details> means it opens
   without JS, is findable by the browser's own in-page search, and
   announces its own state — so the caret is aria-hidden and purely
   drawn.

   D17 adds .who__ph: the phone has no hero duo (duo.css), so each
   operator's portrait sits in this section at 160px square with a 4px
   inset rule in their own field colour. It is display:none above 640,
   where the hero carries the faces instead.
   ============================================================ */

.pairgrid{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:auto auto auto;
  gap:0 var(--gutter)}
.who{display:grid;grid-row:span 3;grid-template-rows:subgrid;row-gap:0;align-content:start}
@supports not (grid-template-rows: subgrid){
  .pairgrid{grid-template-rows:none}
  .who{display:block;grid-row:auto}
}

/* >640 the hero's .duo carries the faces, so this one is not rendered */
.who__ph{display:none;width:160px;height:160px;object-fit:cover;object-position:center 20%;
  margin-bottom:var(--gap);filter:grayscale(1) contrast(1.05)}
/* an INSET shadow: the rule is drawn inside the 160px box, so the two
   portraits stay on one grid whichever operator you read first */
.who--product .who__ph{box-shadow:inset 0 -4px 0 var(--product)}
.who--growth .who__ph{box-shadow:inset 0 -4px 0 var(--growth)}

.who__head{display:flex;align-items:center;gap:var(--gap);flex-wrap:wrap}
/* a slab in the operator's own field colour, not a heading on the ground */
.who__slab{padding:9px 13px;font-variation-settings:'wdth' 104,'wght' 800;
  font-size:var(--fs-action);letter-spacing:-.02em}
.who--product .who__slab{background:var(--product);color:var(--white)}   /* 5.91:1 */
.who--growth .who__slab{background:var(--growth);color:var(--ink)}       /* 5.85:1 */
.who__role{font-variation-settings:'wdth' 100,'wght' 700;font-size:var(--fs-control);margin:0}
.who__li{margin-left:auto;width:44px;height:44px;display:grid;place-items:center;
  background:var(--ink);color:var(--white);
  transition:background-color .18s var(--ease-out), color .18s var(--ease-out)}
.who__li svg{width:17px;height:17px;fill:currentColor;display:block}
@media (hover:hover) and (pointer:fine){.who__li:hover{background:var(--growth);color:var(--ink)}}
.who__bio{font-size:clamp(1rem,1.15vw,1.12rem);line-height:1.5;margin:var(--gap) 0 0;max-width:52ch}

.led{margin:calc(var(--mod) * 1.6) 0 0;padding:0;list-style:none;border-top:2px solid var(--ink)}
.led__row{display:grid;grid-template-columns:calc(var(--mod) * 6.5) minmax(0,1fr);gap:var(--gap-lg);
  align-items:start;padding-block:calc(var(--mod) * 1.1);border-bottom:1px solid var(--hair)}
.led__logo{height:calc(var(--mod) * 1.15);width:auto;max-width:100%;display:block;margin-top:.35em;
  filter:grayscale(1) brightness(0);opacity:.7;
  transition:filter .2s var(--ease-out), opacity .2s var(--ease-out)}
@media (hover:hover) and (pointer:fine){.led__row:hover .led__logo{filter:none;opacity:1}}
@media (hover:none){.led__logo{filter:none;opacity:1}}
.led__fig{display:block;font-variation-settings:'wdth' 108,'wght' 800;font-size:var(--fs-row);
  letter-spacing:-.03em;line-height:1.05}
.led__note{display:block;margin-top:8px;font-size:var(--fs-control);line-height:1.4;
  color:var(--ink-2);max-width:44ch}

/* the disclosure. min-width:0 so a long figure wraps inside the grid
   column instead of widening it. */
.led__d{min-width:0}
.led__sum{display:flex;align-items:center;gap:.5em;cursor:pointer;list-style:none;
  -webkit-tap-highlight-color:transparent}
/* both markers: WebKit uses the pseudo-element, everything else ::marker */
.led__sum::-webkit-details-marker{display:none}
.led__sum::marker{content:""}
/* <summary> is the focus target, and the ring pair would be drawn round
   a full-width row; an offset ink rule is what the source draws. */
.led__sum:focus-visible{outline:2px solid var(--ink);outline-offset:4px}
.led__sum .led__fig{border-bottom:2px solid transparent;
  transition:border-color .18s var(--ease-out)}
@media (hover:hover) and (pointer:fine){
  .led__sum:hover .led__fig{border-bottom-color:var(--ink)}
  .led__sum:hover .led__caret{opacity:1}
}
/* drawn from two borders, not a glyph: it rotates 180 degrees to point
   up when the row is open, and em units keep it tied to the figure */
.led__caret{flex:none;width:.46em;height:.46em;
  border-right:2.5px solid var(--ink);border-bottom:2.5px solid var(--ink);
  transform:translateY(-.16em) rotate(45deg);opacity:.5;
  transition:transform .22s var(--ease-out),opacity .18s var(--ease-out)}
.led__d[open] .led__caret{transform:translateY(.06em) rotate(225deg);opacity:1}
.led__d[open] .led__note{animation:ledNote .24s var(--ease-out) both}
@keyframes ledNote{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){
  .led__caret{transition:none}
  .led__d[open] .led__note{animation:none}
}

@media (max-width:900px){
  .pairgrid{grid-template-columns:1fr;grid-template-rows:none;row-gap:calc(var(--mod) * 3)}
  .who{display:block;grid-row:auto}
}
@media (max-width:640px){
  .led__row{grid-template-columns:1fr;gap:10px;padding-block:18px}
  /* 🔴 .who__slab is an <h3> and NEITHER this file nor the desktop source
     resets its UA margin — at 1440 that is correct, because the desktop
     design renders with the same 1em top and bottom and the two agree. The
     phone source resets it (`margin:0`), and a flex line does not collapse
     margins, so carrying the UA margin here added ~16px above the name slab
     and ~16px below it, twice per operator. It is scoped to this breakpoint
     for exactly that reason: removing it globally would move the desktop
     away from the design it matches. nowrap is the phone source's too — the
     slab is a name and must not break across two lines. */
  .who__slab{margin:0;white-space:nowrap}
  /* the phone source sets a flat 18px logo with no optical nudge; the
     module-derived height plus .35em of margin-top stood every ledger row
     ~3px tall, five rows of it */
  .led__logo{height:18px;margin-top:0;opacity:.8}
  /* D17: the phone's portraits, and the 48px column gap between the
     two operators that the phone source sets instead of 3 modules */
  .who__ph{display:block}
  .pairgrid{row-gap:48px}
  .led{margin-top:26px}
  .who__bio{font-size:1.02rem;margin-top:var(--gap);max-width:none}
  .led__note{max-width:none}
  /* 44px is the tap target; the summary is the only control in the row */
  .led__sum{min-height:44px}
  /* the phone source draws the caret a step stronger (.6 against the
     desktop's .5) and gives it no opacity transition at all — there is no
     hover on a phone to fade towards, so the only opacity change left is the
     jump to 1 on [open], and D15 says add no motion a wireframe does not
     have. The note's 6px is the phone's too; it shows only once a row is
     disclosed, which is why neither the SSIM gate nor the eye caught it. */
  .led__caret{opacity:.6;transition:transform .22s var(--ease-out)}
  .led__note{margin-top:6px}
}

/* ==== design-system/components/listrow.css ==== */
/* ============================================================
   listrow — the numbered, fillable row

   The page's primary list grammar: a 2px ink rule top and bottom, a
   CATEGORY, a name with a sub-line, and the commitment as a chip. On
   hover the whole row fills with one of the three colour fields, the
   contents shift .4 of a module, and the chip flips from outline to
   solid white.

   <a class="listrow listrow--growth" href="/contact">
     <span class="listrow__n">Fixed scope</span>
     <span>
       <span class="listrow__name">GTM + Positioning Sprint</span>
       <span class="listrow__what">A focused assessment of your …</span>
     </span>
     <span class="btn btn--outline btn--sm listrow__end">Fixed project</span>
   </a>

   The Sep 22 redesign replaced the 01/02/03 numerals with a category
   label ("Fixed scope", "Inside the team", "Strategy + Coaching"). That
   is why the column is 11ch rather than 5ch, why the label is set at
   --fs-micro with .08em tracking in caps, and why it is align-self:start
   — a two-word label wraps and must stay level with the name's first
   line instead of centring against three lines of sub-copy. The caps
   here are a SHORT LABEL, not a passage; D20 records the owner keeping
   them, and the all-caps-body hook finding is an accepted ignore.

   The fill is a MODIFIER, not :nth-child — reordering rows must not
   re-colour them (D7). The shipped order is growth, product, overlap.
   ============================================================ */

.listrow{
  display:grid;grid-template-columns:11ch minmax(0,1fr) auto;
  gap:var(--gap);align-items:center;
  padding-block:calc(var(--mod) * 1.2);
  padding-inline:calc(var(--mod) * .8);
  margin-inline:calc(var(--mod) * -.8);   /* the fill bleeds past the text edge */
  border-bottom:2px solid var(--ink);
  text-decoration:none;position:relative;overflow:hidden;
  /* 🔴 own stacking context, so the z-index:-1 fill stays behind the
     row's content but IN FRONT of the section background. Without
     this the fill vanishes the moment the band paints a background,
     and the failure is silent — the text still swaps colour. */
  isolation:isolate;
}
.listrow:first-child{border-top:2px solid var(--ink)}
.listrow:focus-visible{outline-offset:-3px}

.listrow::before{
  content:'';position:absolute;inset:0;z-index:-1;background:var(--growth);
  transform:scaleX(0);transform-origin:left;transition:transform .28s var(--ease-out);
}
.listrow--growth::before {background:var(--growth)}
.listrow--product::before{background:var(--product)}
.listrow--overlap::before{background:var(--overlap)}

.listrow__n{font-variation-settings:'wdth' 100,'wght' 700;font-size:var(--fs-micro);
  color:var(--ink-2);letter-spacing:.08em;text-transform:uppercase;line-height:1.3;
  align-self:start;padding-top:.35em}
.listrow__name{font-variation-settings:'wdth' 108,'wght' 780;font-size:var(--fs-row);
  letter-spacing:-.03em;line-height:1.05}
.listrow__what{display:block;font-variation-settings:'wdth' 100,'wght' 400;
  font-size:var(--fs-control);color:var(--ink-2);margin-top:8px;max-width:48ch;
  /* 🔑 explicit. Without it the sub-line inherits .listrow__name's
     display line-height of 1.05 and sets solid. */
  line-height:1.4;letter-spacing:0}

.listrow__n,.listrow__name,.listrow__what{
  transition:color .22s var(--ease-out), transform .28s var(--ease-out);
}
/* the chip swaps its colours at .2s, a notch quicker than the type's .22s, so
   the outline-to-solid flip lands with the fill instead of trailing it. Both
   values are the wireframe's (.svc__shape vs .svc__n/__name/__what); the
   design system had merged the four into one rule and given the chip .22s. */
.listrow__end{
  transition:background-color .2s var(--ease-out), color .2s var(--ease-out),
             border-color .2s var(--ease-out), transform .28s var(--ease-out);
}

@media (hover:hover) and (pointer:fine){
  .listrow:hover::before{transform:scaleX(1)}
  .listrow:hover .listrow__n,
  .listrow:hover .listrow__name,
  .listrow:hover .listrow__what,
  .listrow:hover .listrow__end{transform:translateX(calc(var(--mod) * .4))}

  /* the chip flips: outline on the ground, solid white on the fill */
  .listrow:hover .listrow__end{background:var(--white);border-color:var(--white);color:var(--ink)}

  /* orange is the base fill, so the orange text colours are the base
     too and the two modifiers below override them. A bare .listrow is
     then legible on its own fill instead of keeping ink-2 on orange.
     Ink reads 5.85:1; the dim sub-line uses --on-growth 4.89:1. */
  .listrow:hover .listrow__name{color:var(--ink)}
  .listrow:hover .listrow__n,
  .listrow:hover .listrow__what{color:var(--on-growth)}

  /* blue fill: white reads 5.91:1 */
  .listrow--product:hover .listrow__n,
  .listrow--product:hover .listrow__name,
  .listrow--product:hover .listrow__what{color:var(--white)}

  /* near-black fill: --on-overlap reads 13.90:1 */
  .listrow--overlap:hover .listrow__n,
  .listrow--overlap:hover .listrow__name,
  .listrow--overlap:hover .listrow__what{color:var(--on-overlap)}
}

/* touch: there is no hover to give, so the rows are filled at rest.
   The row rule goes white because a 2px ink rule disappears into an
   overlap fill and cuts an orange one in half. */
@media (hover:none){
  .listrow::before{transform:scaleX(1)}
  .listrow{border-bottom-color:var(--white)}
  .listrow .listrow__end{background:var(--white);border-color:var(--white);color:var(--ink)}
  .listrow .listrow__n,
  .listrow .listrow__what{color:var(--on-growth)}
  .listrow--product .listrow__n,
  .listrow--product .listrow__name,
  .listrow--product .listrow__what{color:var(--white)}
  .listrow--overlap .listrow__n,
  .listrow--overlap .listrow__name,
  .listrow--overlap .listrow__what{color:var(--on-overlap)}
}

@media (prefers-reduced-motion:reduce){
  .listrow::before{transition:none}
  .listrow:hover .listrow__n,.listrow:hover .listrow__name,
  .listrow:hover .listrow__what,.listrow:hover .listrow__end{transform:none}
}

@media (max-width:900px){
  /* ONE column, not a narrow label column beside the name: an 11ch
     category wraps to three lines in a 4ch track. The label leads the
     row instead, and the chip closes it. */
  .listrow{grid-template-columns:minmax(0,1fr);row-gap:calc(var(--mod) * .6);align-items:start}
  .listrow__n{padding-top:0}
  .listrow__end{grid-column:1;justify-self:start}
}

/* D17 — the phone fills the rows BY BREAKPOINT as well as by pointer.
   🔴 The (hover:none) branch above is not enough on its own. It asks
   what the INPUT can do, and a 390px window on a laptop — which is what
   the fidelity gate renders, and what anyone gets by narrowing a desktop
   browser — still reports hover:hover and pointer:fine. The rows would
   sit unfilled at the exact width the phone design draws them filled.
   The two conditions are independent and both are real, so both are
   written. Values duplicated rather than factored into a shared class:
   a media query cannot be OR-ed, and a third class carried in the markup
   would have to be kept in sync with two breakpoints by hand. */
@media (max-width:640px){
  .listrow::before{transform:scaleX(1)}
  .listrow{border-bottom-color:var(--white);
    padding-inline:var(--gutter);margin-inline:calc(var(--gutter) * -1);
    padding-block:20px;row-gap:10px}
  .listrow:first-child{border-top:0}
  /* 🔑 border:0, not just a white border-colour. The chip is .btn--outline,
     whose 2px rule adds 4px to the box; the phone source's .svc__shape is a
     solid white field with no rule at all, so a white 2px border looked
     right and stood every service row 4px tall. Scoped to .listrow__end:
     .btn--outline is also the masthead action at this width, and that one
     keeps its rule. */
  .listrow .listrow__end{background:var(--white);border:0;color:var(--ink)}
  .listrow .listrow__n,
  .listrow .listrow__what{color:var(--on-growth)}
  .listrow--product .listrow__n,
  .listrow--product .listrow__name,
  .listrow--product .listrow__what{color:var(--white)}
  .listrow--overlap .listrow__n,
  .listrow--overlap .listrow__name,
  .listrow--overlap .listrow__what{color:var(--on-overlap)}
  .listrow__what{margin-top:8px;max-width:none}
  /* 🔑 display:block on the NAME, and only at this breakpoint. The desktop
     source leaves .svc__name inline (so does this file, and at 1440 the two
     agree); the phone source blocks it. Inline, the name's line box is the
     max of its own 1.05 leading and the strut of its parent <span>, which
     inherits the body's 1.5 — so the row ran ~2px tall and the sub-line sat
     2px low, three times over. */
  .listrow__name{display:block}
}

/* ==== design-system/components/logos.css ==== */
/* ============================================================
   logos — the client marquee

   <div class="clients">
     <p class="clients__label">Clients we've worked with</p>
     <div class="clients__marquee">
       <div class="clients__track">
         <ul class="clients__set">
           <li><img class="logo logo--wide" src="/assets/logos/betterfly.png"
                    alt="Betterfly" width="526" height="96"></li>
         </ul>
         <ul class="clients__set" aria-hidden="true">…</ul>   x3 more
       </div>
     </div>
   </div>

   Proof, in the hero, on its own ground: no band and no rules, so it
   reads as part of the section above rather than a strip bolted on.
   Source files must be FULL COLOUR — the row flattens them to ink in
   CSS (grayscale(1) brightness(0), 82%), so photography and logos sit
   in the same tonal register as the fields.

   D20: the marquee stays. It is CSS ONLY — one translate on
   .clients__track, no script, nothing to synchronise. FOUR sets, of
   which the first is the real list and the other three carry
   aria-hidden and alt="": the loop travels -25%, which is exactly one
   set's width, so the seam lands where the next set begins and the
   walk is seamless at any viewport. A screen reader and a text
   extractor read the four logos once.

   The mask fades both edges instead of cutting them, so a logo leaves
   the strip rather than being clipped mid-wordmark.

   Reduced motion is not "slower": the track stops being a track. The
   animation is removed, the duplicate sets are display:none, and the
   one real set wraps — which is the STATIC row this component replaced,
   so nothing is lost and no logo is unreachable.

   D4: the logos ship BARE. The Sep 20 wireframe put an --fs-micro role
   under each one; the owner removed it, so .clients__role is gone from
   here and from rename-map.json. .clients__row went with the Sep 22
   redesign: the reduced-motion branch below IS that wrapped row, so
   keeping a second class for it would have been two names for one thing.

   The employer row (.exp, a grey logo that revealed a figure in a
   hover tooltip) is GONE. Figures-Are-Attributed: every figure is
   visible at rest, in the ledger — see ledger.css.
   ============================================================ */

.clients{margin-top:calc(var(--mod) * 2.8)}
.clients__label{font-variation-settings:'wdth' 100,'wght' 700;font-size:var(--fs-small);
  color:var(--ink-2);margin:0 0 var(--gap)}

.clients__marquee{overflow:hidden;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent)}
.clients__track{display:flex;width:max-content;align-items:center;
  animation:clientsRoll 34s linear infinite}
/* the one hover this component has: the strip holds still to be read */
.clients__marquee:hover .clients__track{animation-play-state:paused}
@keyframes clientsRoll{from{transform:translate3d(0,0,0)}to{transform:translate3d(-25%,0,0)}}
.clients__set{display:flex;align-items:center;list-style:none;margin:0;padding:0;
  gap:clamp(40px,5vw,calc(var(--mod) * 4));padding-right:clamp(40px,5vw,calc(var(--mod) * 4))}
.clients__set li{display:block}

/* 🔑 height plus width:auto, and the markup carries width/height
   attributes for CLS. An <img> width/height maps to a presentational
   height that beats aspect-ratio, so height here is what wins. */
.logo{height:calc(var(--mod) * 1.5);width:auto;display:block;
  filter:grayscale(1) brightness(0);opacity:.82}
/* optical size, not measured size: a long wordmark reads larger at
   the same height, so it gets set smaller */
.logo--wide{height:calc(var(--mod) * 1.2)}

/* D17, the phone design: a narrower mask, a tighter set and a quicker
   walk, because the strip is 390px wide instead of 1440 and the same
   34s would read as stalled. The loop distance does not change — it is
   a property of the four sets, not of the viewport. */
@media (max-width:640px){
  .clients{margin-top:44px}
  .clients__marquee{
    -webkit-mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);
    mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent)}
  .clients__track{animation-duration:26s}
  .clients__set{gap:36px;padding-right:36px}
  .logo{height:22px}
  .logo--wide{height:18px}
}

/* 🔑 LAST, after the breakpoint above. Written before it, the phone's
   gap:36px out-cascaded the wrapped gap below at the same specificity
   and a reduced-motion phone got a 36px wrapped row instead of the
   18px/28px one both design sources draw. Reduced motion is not a
   modifier on the breakpoint; it outranks it. */
@media (prefers-reduced-motion:reduce){
  .clients__track{animation:none;width:100%;flex-wrap:wrap}
  .clients__set:not(:first-child){display:none}
  .clients__set{flex-wrap:wrap;gap:calc(var(--mod) * 1.2) clamp(28px,3.4vw,calc(var(--mod) * 3))}
}
@media (prefers-reduced-motion:reduce) and (max-width:640px){
  .clients__set{gap:18px 28px;padding-right:0}
}

/* ==== design-system/components/panel.css ==== */
/* ============================================================
   panel — the working surface, the next-steps aside, the calendar

   <div class="panel">…the form…</div>

   <aside class="next" aria-label="What happens next">
     <div class="next__blue">
       <h2 class="next__k">What happens next</h2>
       <ol>
         <li><span class="next__n num">1</span><span><b>You send this</b>
           <span>It comes straight to the two of us.</span></span></li>
       </ol>
     </div>
     <div class="next__orange"><p>If we're not the right fit, we'll tell you.</p></div>
   </aside>

   .panel is white with a 2px ink rule — the only container shape in the
   system besides a field (there are no cards, and no shadow in any
   state).

   .next is the signature at aside scale: a blue field holding the
   sequence, an orange field crossing its foot, and the crossing painting
   itself --overlap. .next owns the stacking context so the multiply is
   scoped to the two fields. Both fields pad their text clear of the
   crossing: the blue one 3.6 modules at the bottom, the orange one 3 at
   the top, so nothing inked ever lands on --overlap.

   .cal is the calendar panel: .panel with its padding removed, because
   the iframe is the surface. It is declared AFTER .panel's media query
   on purpose — same specificity, so source order is what makes
   padding:0 hold at every width.

   .got is the booking banner, shown only with ?from=contact. It is
   display:none here and the page's script sets display:flex, which is
   why the flex properties are declared on the hidden state.
   ============================================================ */

.panel{background:var(--white);border:2px solid var(--ink);padding:calc(var(--mod) * 1.8)}

.next{position:relative;isolation:isolate}
.next__blue{background:var(--product);color:var(--white);
  padding:calc(var(--mod) * 1.8);padding-bottom:calc(var(--mod) * 3.6)}
.next__k{font-variation-settings:'wdth' 108,'wght' 800;font-size:var(--fs-phase);
  letter-spacing:-.025em;line-height:1.1;margin:0}
.next ol{list-style:none;margin:calc(var(--mod) * 1.2) 0 0;padding:0}
.next li{display:grid;grid-template-columns:3ch 1fr;gap:var(--gap);
  padding-block:calc(var(--mod) * .85);border-top:1px solid var(--on-product)}
.next li:first-child{border-top:0;padding-top:0}
.next__n{font-variation-settings:'wdth' 100,'wght' 800;font-size:var(--fs-control)}
.next li b{display:block;font-variation-settings:'wdth' 104,'wght' 800;
  font-size:var(--fs-list);letter-spacing:-.01em}
.next li span span{display:block;color:var(--on-product);font-size:var(--fs-control);
  line-height:1.4;margin-top:3px}
.next__orange{position:relative;margin-top:calc(var(--mod) * -2);margin-left:calc(var(--mod) * 3);
  margin-right:calc(var(--mod) * -1.2);
  background:var(--growth);color:var(--ink);mix-blend-mode:multiply;
  padding:calc(var(--mod) * 3) calc(var(--mod) * 1.6) calc(var(--mod) * 1.4)}
.next__orange p{margin:0;font-variation-settings:'wdth' 104,'wght' 800;font-size:var(--fs-list);
  letter-spacing:-.01em;line-height:1.3;max-width:26ch}

/* the orange field wipes in from the left, over the blue one it crosses.
   Geometry only; the field's paint is in plain CSS above. */
@media (prefers-reduced-motion:no-preference){
  .js .next__orange{animation:wipe-l .7s var(--ease-out) .35s backwards}
}

@media (max-width:900px){.next__orange{margin-right:0}}
@media (max-width:640px){
  .panel{padding:var(--pad-cell);margin-inline:0}
  .next__orange{margin-left:calc(var(--mod) * 1.5)}
}

/* ---- the calendar panel (declared after .panel's media query) ---- */
.cal{padding:0;overflow:hidden}
.cal iframe{display:block;width:100%;height:700px;border:0}
.cal__alt{font-size:var(--fs-small);color:var(--ink-2);margin:var(--gap) 0 0}

/* ---- the booking banner ---- */
.got{display:none;align-items:center;gap:.7em;background:var(--product);color:var(--white);
  padding:11px 15px;margin:0 0 var(--gap-lg);width:fit-content;max-width:100%;
  font-variation-settings:'wdth' 104,'wght' 800;font-size:var(--fs-control)}
.got svg{width:18px;height:18px;flex:0 0 auto;fill:none;stroke:currentColor;
  stroke-width:2.4;stroke-linecap:square}

/* ==== design-system/components/phase.css ==== */
/* ============================================================
   phase — the sequenced process, on the dark band

   <section class="process" id="process">
     <div class="sheet">
       <h2 class="mega">From fundamentals<br>to handover</h2>
       <p class="process__lede">…</p>
       <div class="track" aria-hidden="true"><span class="track__fill"></span></div>
       <div class="phases">
         <div class="phase">
           <span class="phase__n num">01</span>
           <h3 class="phase__name">Know what's true</h3>
           <p class="phase__what">…</p>
         </div>  x4
       </div>
     </div>
   </section>

   Moment 3 of 3. When the phases enter view one walk lights 01 to 04 at
   950ms a step — the lit phase at full opacity with a 2px orange tick
   drawn down from the track, the track fill advancing by scaleX — then
   every phase settles at full opacity and STAYS there. There is no
   hover dimming after the run; it was built and removed.

   Built for the overlap ground. Orange measures 5.59:1 there; blue is
   2.93:1 and never appears on it (Large-Field Orange Rule).

   Needs system.js. Without JS, or under reduced motion, everything is
   lit from the first frame — never leave phases dimmed with no way up.
   ============================================================ */

/* the band itself: full-bleed overlap, its own sheet inside */
.process{background:var(--overlap);color:var(--white);
  padding-block:calc(var(--mod) * 5);overflow-x:clip}
.process .mega{color:var(--white)}
.process__lede{color:var(--on-overlap);max-width:50ch;margin:var(--gap-lg) 0 0;
  font-size:clamp(1.02rem,1.2vw,1.18rem)}

.track{position:relative;height:2px;background:rgba(255,255,255,.18);margin-top:calc(var(--mod) * 3)}
.track__fill{position:absolute;inset:0;background:var(--growth);transform-origin:left;
  transform:scaleX(0);transition:transform .75s var(--ease-out)}

.phases{
  display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:auto auto auto;
  column-gap:var(--gutter);row-gap:0;margin-top:calc(var(--mod) * 1.6);
}

/* subgrid lands each phase's three parts on shared rows, so every
   description starts on the same baseline across columns.
   🔑 row-gap:0 stops the subgrid inheriting the parent's gap and
   splitting one phase into three loose lines. */
.phase{display:grid;grid-row:span 3;grid-template-rows:subgrid;row-gap:0;align-content:start;
  padding-top:calc(var(--mod) * 1.3);position:relative;
  /* .66 is the LOWEST dim that still passes on this ground:
     description 6.6:1, numeral 3.1:1 as large text. The Sep 18 value of
     .34 measured 2.73:1 and 1.69:1 and failed both. */
  opacity:.66;transition:opacity .3s var(--ease-out)}
@supports not (grid-template-rows: subgrid){
  .phases{grid-template-rows:none}
  .phase{grid-row:auto;display:block}
}

/* a tick dropping from the rule, drawn as each phase is reached */
.phase::before{content:'';position:absolute;top:0;left:0;width:2px;height:calc(var(--mod) * .9);
  background:var(--growth);transform:scaleY(0);transform-origin:top;
  transition:transform .4s var(--ease-out)}

.phase.lead{opacity:1}
.phase.lead::before{transform:scaleY(1)}

/* after the run nothing stays dimmed, and nothing re-dims on hover */
.phases.settled .phase{opacity:1}
.phases.settled .phase::before{transform:scaleY(1)}

.phase__n{font-variation-settings:'wdth' 100,'wght' 800;font-size:var(--fs-phase-n);
  color:var(--growth);line-height:1;letter-spacing:-.04em}
.phase__name{font-variation-settings:'wdth' 108,'wght' 780;font-size:var(--fs-phase);
  letter-spacing:-.025em;line-height:1.15;margin:calc(var(--mod) * .7) 0 0;align-self:start}
.phase__what{color:var(--on-overlap);font-size:var(--fs-control);
  margin:calc(var(--mod) * .7) 0 0;max-width:32ch;align-self:start}

/* ---- D28: the hover state ---------------------------------------------
   The whole column is the target. The numeral goes orange -> white, and the
   short tick above the column extends to the full column width.

   🔑 The extension is a SECOND pseudo-element, not the walk's tick. ::before
   is the walk: its transform is scaleY (the tick being drawn down from the
   track) on a .4s curve that system.js orchestrates, and one element cannot
   carry two transitions of the same property at two durations. ::after is the
   full-column bar, laid over the tick's own box (same top, same height, same
   orange), scaleX(0) at rest and scaleX(1) on hover at D28's 200ms. At rest it
   has zero width, so nothing about the page's resting render changes -- which
   is what keeps the US-011 parity score where it is -- and on hover the tick
   and the bar are one continuous mark.

   left:0/right:0 rather than a scale factor: the bar is exactly the column,
   at every width the 4-column, 2-column and 1-column grids produce, without a
   number in this file that has to track the layout.

   hover:hover AND pointer:fine: a touch device gets no hover state and loses
   nothing -- the numerals are already legible, and the tick is already down.
   ============================================================ */
@media (hover:hover) and (pointer:fine){
  .phase::after{content:'';position:absolute;top:0;left:0;right:0;
    height:calc(var(--mod) * .9);background:var(--growth);
    transform:scaleX(0);transform-origin:left;
    transition:transform .2s var(--ease-out)}
  .phase:hover::after{transform:scaleX(1)}
  .phase:hover .phase__n{color:var(--white)}
  .phase__n{transition:color .2s var(--ease-out)}
}

@media (prefers-reduced-motion:reduce){
  /* no walk: every phase at full strength from the first paint */
  .phase{opacity:1;transition:none}
  .phase::before{transform:scaleY(1);transition:none}
  .track__fill{transform:scaleX(1);transition:none}
  /* D28: the colour change stays, the transform is dropped. The bar never
     extends, so there is no motion to reduce -- and the hover still SAYS
     something, which a state that only moved would not. */
  .phase::after{transition:none}
  .phase:hover::after{transform:scaleX(0)}
  .phase__n{transition:none}
}

@media (max-width:1100px){.phases{grid-template-columns:repeat(2,1fr);row-gap:calc(var(--mod) * 2.4)}}

/* D17 — the phone design has NO WALK.
   🔴 This is not a style preference, it is the only honest reading of
   the phone source: its process section draws every phase at full
   strength with its tick already down and the rule solid orange. There
   is no track fill and no orchestration in it at all.
   system.js still runs here — it is one page and one script — so the
   settled state has to be stated in CSS rather than waited for, or a
   phone that screenshots or prints in the first second of the walk
   catches three phases at .66 and a rule that is 25% painted. Every
   path (walk, .lead, .settled, reduced motion, no JS) already ends at
   full opacity; this makes 640-and-below START there. */
@media (max-width:640px){
  .phases{grid-template-columns:1fr;row-gap:34px;margin-top:26px}
  /* display:block, so the subgrid goes with the columns. Four phases in ONE
     column have nothing left to align across, and the subgrid was not free:
     it made .phase a grid container, which BLOCKIFIES the .phase__n <span>
     into a grid item. Blockified, the numeral's box is exactly its 1.0
     leading; inline — which is what the phone source renders, its .phase
     being a plain block — the line box is resolved against the parent's
     strut instead, and the two land 1px apart. Times four phases and the
     dark band ran 3px long. */
  .phase{opacity:1;padding-top:20px;display:block;grid-row:auto}
  .phase::before{transform:scaleY(1);height:14px}
  /* D28's bar tracks the tick's height here too: below 640 the tick is a
     flat 14px (the phone source's value) while --mod * .9 is 12.6px. A
     narrow DESKTOP window still reports hover:hover, so the two have to
     agree at this width as well -- the same independence listrow.css
     records between the pointer and the breakpoint. */
  .phase::after{height:14px}
  .track{background:var(--growth);margin-top:40px}
  .track__fill{display:none}
  .phase__n{font-size:2.1rem}
  .phase__name{margin-top:10px}
  .phase__what{margin-top:8px;max-width:none}
  .process{padding-block:64px}
  .process__lede{font-size:1.05rem;margin-top:var(--gap-lg);max-width:none}
}

/* ==== design-system/components/reveal.css ==== */
/* ============================================================
   reveal — the one entrance, for ROWS only

   <div class="reveal" style="--d:70ms">…</div>

   Belief columns, ledger rows, list rows and fit cards: 0.5s rise and
   fade, staggered 60–140ms with --d. Headings and ledes never take it
   (Do's and Don'ts), and there is no second entrance pattern.

   Gated on .js, so a no-JS page renders everything visible. system.js
   adds .js to <html> and carries a 1.6s failsafe that reveals
   everything regardless — a reveal that never fires is content that
   never exists.

   🔑 system.js REMOVES both classes about 0.9s after the row has
   played, and clears --d. It has to: `.js .reveal.is-in` at (0,3,0)
   out-specified the components' own hover and press transitions in the
   Sep 18 build and turned a 120ms button press into 550ms. The class
   coming off is what makes the transition-delay above harmless.
   ============================================================ */

.js .reveal{opacity:0;transform:translateY(calc(var(--mod) * .9))}
.js .reveal.is-in{opacity:1;transform:none;
  transition:opacity .5s var(--ease-out), transform .5s var(--ease-out);
  transition-delay:var(--d,0ms)}

@media (prefers-reduced-motion:reduce){
  .js .reveal{opacity:1;transform:none}
  .js .reveal.is-in{transition:none}
}

/* D15 + D17: add no motion a wireframe does not have. The phone source
   carries no script and no entrance — every row is present from the
   first paint — so the reveal is switched off at the phone breakpoint
   rather than played at a width it was never designed for. Same shape
   as the reduced-motion branch above: the resting state IS the visible
   one, so switching it off can only ever reveal content. */
@media (max-width:640px){
  .js .reveal{opacity:1;transform:none}
  .js .reveal.is-in{transition:none}
}

/* ==== design-system/components/stage.css ==== */
/* ============================================================
   stage — the crossing fields at section scale (signature)

   <section class="band band--tint why" id="why">
     <div class="sheet">
       <div class="stage">
         <span class="stage__f stage__f--p" aria-hidden="true"></span>
         <span class="stage__f stage__f--g" aria-hidden="true"></span>
         <div class="stage__t stage__t--p">
           <h3 class="stage__k">Go-to-market &amp; Growth</h3>
           <div class="stage__qblock">
             <p class="stage__sub">Some of the questions we will help you answer</p>
             <ul class="stage__qs"><li>How do I get more leads…</li></ul>
           </div>
         </div>
         <div class="stage__t stage__t--o">…the fundamentals…</div>
         <div class="stage__t stage__t--g">…Product &amp; Positioning…</div>
       </div>
     </div>
   </section>

   A blue field on the left 62% and an orange field on the right 62%,
   both multiplied, so the middle 24% paints --overlap. Three text
   regions sit ABOVE the blend: Go-to-market & Growth (white), Product &
   Positioning (ink, right-aligned) and, in the crossing, the
   fundamentals (--on-overlap, with a white key).

   The Sep 22 redesign replaced each side's one-line .stage__q with a
   QUESTION BLOCK: a label and the three questions that side answers,
   ruled between. The rules are drawn in `currentColor` at 32% rather
   than a fixed white, because the two sides carry opposite text
   colours and one hard-coded rule colour would vanish on one of them.
   The block is measure-capped at 30ch and, on the right-hand side,
   pushed to the field's own edge with margin-left:auto so the two
   blocks mirror each other instead of both hugging the left.

   The stage grew from 17 to 20 modules (320px to 380px floor) to hold
   them; that height is the wireframe's, not a guess.

   Moment 2 of 3 is scroll-linked and lives in system.js: it moves the
   two fields TOWARD the position this stylesheet already holds (from 8%
   of the stage width apart to closed), and clips the crossing's text to
   the crossing's current width. It runs at 761px and up, without
   reduced motion. If it never runs, nothing is missing
   (Moves-Toward-Rest).

   At 760px and below there is no room to cross side by side, so the
   fields are removed and the three regions stack as solid blue, overlap
   and orange blocks — the static --overlap hex doing the one job it
   exists for.

   The why band must carry overflow-x:clip: the fields start outside
   their resting place, which at 1440 is wider than the gutter
   (Bleed-Without-Scroll).
   ============================================================ */

.why{overflow-x:clip}

.stage{position:relative;isolation:isolate;height:calc(var(--mod) * 20);min-height:380px}

.stage__f{position:absolute;top:0;bottom:0;width:62%;mix-blend-mode:multiply;will-change:transform}
.stage__f--p{left:0;background:var(--product)}
.stage__f--g{right:0;background:var(--growth)}

.stage__t{position:absolute;top:0;bottom:0;padding:var(--pad-cell);
  display:flex;flex-direction:column;justify-content:space-between;gap:var(--gap)}
.stage__t--p{left:0;width:38%;color:var(--white)}
.stage__t--g{right:0;width:38%;color:var(--ink);text-align:right;align-items:flex-end}
.stage__t--o{left:38%;width:24%;color:var(--on-overlap)}

.stage__k{font-variation-settings:'wdth' 108,'wght' 800;font-size:var(--fs-row);
  letter-spacing:-.03em;line-height:1;margin:0}
.stage__t--o .stage__k{font-size:var(--fs-phase);color:var(--white)}
.stage__qblock{max-width:30ch;width:100%}
.stage__t--g .stage__qblock{margin-left:auto}
.stage__sub{margin:0 0 calc(var(--mod) * .5);font-size:var(--fs-micro);line-height:1.35;
  letter-spacing:.04em;text-transform:uppercase;max-width:none}
.stage__qs{list-style:none;margin:0;padding:0;font-size:var(--fs-small);line-height:1.35;max-width:none}
/* currentColor, not white: the blue side sets white and the orange side
   sets ink, and one fixed rule colour disappears on one of them. */
.stage__qs li{padding-block:calc(var(--mod) * .38);border-top:1px solid currentColor;
  border-top-color:color-mix(in srgb,currentColor 32%,transparent)}
.stage__qs li:first-child{border-top:0}
.stage__list{list-style:none;margin:0;padding:0;font-size:var(--fs-list);line-height:1.3}
.stage__list li{padding-block:calc(var(--mod) * .42);border-top:1px solid rgba(255,255,255,.26)}
.stage__list li:first-child{border-top:0}

@media (max-width:760px){
  .stage{height:auto;min-height:0;display:grid}
  .stage__f{display:none}
  .stage__t{position:static;width:auto;text-align:left;align-items:flex-start}
  .stage__t--p{background:var(--product)}
  .stage__t--o{background:var(--overlap)}
  .stage__t--g{background:var(--growth)}
}

/* D17, the phone design. The regions stop being a crossing and become
   three stacked blocks, so the question block stops being a measure
   inside a 38% column and simply fills its block.
   🔑 max-width:none is REQUIRED, not tidying: 30ch at the phone's 16px
   root is ~240px inside a ~350px block, so the cap that mirrors the two
   sides at 1440 strands a third of the line on a 390px screen. */
@media (max-width:640px){
  /* 🔑 align-items:stretch, undoing the flex-start the 760 rule above sets.
     In a COLUMN flex container align-items is the cross axis, so flex-start
     shrink-wraps every child to its own text width: the five fundamentals
     kept their 1px rules but the rules stopped at "Which numbers to trust"
     (165px) instead of running the full 350px column the phone source draws.
     The 760 rule needs flex-start because the two outer regions are still
     text-aligned at that width; the phone stacks them as plain blocks. */
  .stage__t{padding:22px 20px 24px;gap:18px;justify-content:flex-start;
    align-items:stretch}
  .stage__qblock{max-width:none}
  .stage__t--g .stage__qblock{margin-left:0}
  .stage__sub{margin-bottom:8px}
  .stage__qs li{padding-block:6px}
  .stage__list li{padding-block:7px}
}

/* ==== design-system/components/steps.css ==== */
/* ============================================================
   steps — where you are in the flow, and what comes next

   <ol class="steps" aria-label="Booking steps">
     <li class="is-done"><span class="num">1</span> Tell us</li>
     <li aria-current="step"><span class="num">2</span> Pick a time</li>
   </ol>

   Two states, both carried in markup rather than by script:
   aria-current="step" is the current one and .is-done is behind you.
   Both thicken the rule from 2px --ink-2 to 4px --ink and lift the text
   to full ink, so the state is a change of weight and colour, not a
   colour alone. margin-top:2px on the inactive rule keeps every label
   on one baseline while the rule above it doubles in thickness.
   ============================================================ */

.steps{list-style:none;margin:calc(var(--mod) * 2.2) 0 0;padding:0;
  display:grid;grid-template-columns:1fr 1fr;gap:var(--gap);max-width:34rem}
.steps li{border-top:2px solid var(--ink-2);margin-top:2px;padding-top:10px;
  font-size:var(--fs-control);color:var(--ink-2);display:flex;gap:.6em}
.steps li[aria-current="step"]{border-top:4px solid var(--ink);margin-top:0;color:var(--ink);
  font-variation-settings:'wdth' 100,'wght' 700}
.steps li.is-done{border-top:4px solid var(--ink);margin-top:0;color:var(--ink)}

/* ==== design-system/components/tick.css ==== */
/* ============================================================
   tick — the authored check and cross

   Drawn, not a font glyph and not an emoji. Square caps, 2.4 stroke,
   currentColor, so the mark inherits whatever ground it sits on and the
   ring stays legible on blue and on white. Its 26px box is square
   (Zero-Radius) and 2px-bordered by the modifier.

   <span class="tick tick--on-product" aria-hidden="true">
     <svg viewBox="0 0 24 24"><path d="M4 12.5 9.5 18 20 6.5"/></svg>
   </span>
   cross path: d="M6 6l12 12M18 6L6 18"

   The mark is decorative; the row's own text carries the meaning,
   so it is always aria-hidden. D7: the wireframes call it .mk.
   ============================================================ */

.tick{width:26px;height:26px;display:grid;place-items:center;flex:0 0 auto;margin-top:-1px}
.tick svg{width:13px;height:13px;display:block;fill:none;stroke:currentColor;
  stroke-width:2.4;stroke-linecap:square}

/* on a blue field */
.tick--on-product{border:2px solid rgba(255,255,255,.55);color:var(--white)}
/* receded, on white — the not-a-fit card */
.tick--muted{border:2px solid var(--hair);color:var(--ink-2)}
