/* weyuu.in — site styles.
 *
 * THREE HOUSE RULES, and everything below follows from them.
 *   1. Montserrat, and nothing else. No other face may render the page, so
 *      the stack ends in a generic only to cover the moment before the font
 *      arrives.
 *   2. Black, white and grey, for the THEME and the UI. Every colour value
 *      declared in this file is a pure neutral and there is no accent colour
 *      anywhere in it. That rule governs type, surfaces, borders and
 *      controls; it does NOT govern content. Photographs and video carry
 *      their own colour, as they should.
 *   3. The hero is one bold heading, centred. Nothing else lives in it.
 *
 * MOBILE PRIMARY, and that is a build order rather than a breakpoint. Every
 * rule is written for a phone and left alone; the media queries only ADD as
 * the screen grows. Nothing here undoes a desktop rule for mobile.
 */

/* ── Montserrat, the full family ────────────────────────────────────────── */
/* Variable fonts: one file carries the whole 100 to 900 range, so the full
   family costs two requests for roman and two for italic rather than the
   eighteen a static family would need. Self-hosted rather than linked from
   Google, so the page has no third-party request on first paint and the brand
   face cannot change underneath us.

   font-display: swap on purpose. With `block`, a page that forbids every
   other face would show nothing at all until the font landed. */
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('../fonts/montserrat-latin.woff2') format('woff2');
    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+2074, U+20AC,
                   U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('../fonts/montserrat-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
                   U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF,
                   U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
                   U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Montserrat';
    font-style: italic;
    font-weight: 100 900;
    font-display: swap;
    src: url('../fonts/montserrat-latin-italic.woff2') format('woff2');
    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+2074, U+20AC,
                   U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Montserrat';
    font-style: italic;
    font-weight: 100 900;
    font-display: swap;
    src: url('../fonts/montserrat-latin-ext-italic.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
                   U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF,
                   U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
                   U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ── Tokens ─────────────────────────────────────────────────────────────── */
:root {
    /* Neutrals only. Every one is a pure grey, R equal to G equal to B, so
       nothing can drift warm or cool as more sections are added. */
    --white:    #FFFFFF;
    --grey-100: #F4F4F4;
    --grey-200: #E6E6E6;
    --grey-400: #9B9B9B;
    /* 5.28:1 on white, so it stays legal for body text when sections arrive. */
    --grey-600: #6B6B6B;
    --grey-900: #171717;
    --black:    #000000;

    /* Jade Green — the one brand colour on an otherwise greyscale page, and
       the reason it works is that it is used almost nowhere. It marks "Grow",
       the last rung of the spine, in the h1 and nothing else yet.

       CONTRAST: 2.51:1 on white. That is below WCAG AA for text of ANY size
       (4.5:1 normal, 3:1 large), so it is legal here only because it colours
       one word inside a sentence whose meaning does not depend on the colour
       — remove the green and the h1 still reads identically. Do NOT use it
       for body text, links, a button label, or anything a reader has to read
       to understand the page. If it is ever needed for real text, darken it:
       #009960 clears 3:1 and #007A4D clears 4.5:1, both the same hue. */
    --jade:     #00BB77;

    /* ── ROLE TOKENS ────────────────────────────────────────────────────
       Everything above is the PALETTE (named for what the colour is).
       Everything here is a ROLE (named for what the colour does). Only
       roles may be used in a rule; the palette is raw material.

       That split is what makes the dark theme a ~25-line override at the
       bottom of this block instead of a rewrite: the dark scheme redefines
       these roles and every rule on the page follows, because no rule
       names a grey directly.

       Two deliberate exceptions keep their palette values in both themes:
       .video__play (a white disc with a dark triangle) and .video__btn's
       backdrop. Both sit ON the video poster, not on the page, so they
       must not follow the page. */
    --bg:          var(--white);      /* page background                    */
    --surface:     var(--grey-100);   /* subtle raised fill: chips, callout */
    --card-bg:     var(--white);      /* .story, which needs its own step   */
    --ink:         var(--grey-900);   /* primary text                       */
    --muted:       var(--grey-600);   /* secondary text, the ONLY legal one */
    --faint:       var(--grey-400);   /* decoration only, NEVER text        */
    --line:        var(--grey-200);
    /* The fade behind the text on a photo Growth Story card, as an RGB triple
       so the gradient can vary its alpha. Matches --bg, so in light mode the
       photo fades to white under dark text, and in dark mode to black under
       light text: the card follows the theme like the rest of the page. */
    --story-fade: 255, 255, 255;   /* hairlines                          */
    --solid:       var(--grey-900);   /* filled button                      */
    --solid-hover: var(--black);
    --on-solid:    var(--white);      /* text ON a filled button            */
    --scrim:       rgba(23, 23, 23, .38);
    --card-shadow: 0 1px 2px rgba(0, 0, 0, .05), 0 6px 18px -10px rgba(0, 0, 0, .18);

    --font: 'Montserrat', sans-serif;

    --wrap: 1120px;
    /* The gutter is a token so .bleed can cancel exactly this much and stay
       correct when the breakpoint changes it. */
    --wrap-pad: 20px;

    /* One spacing scale for the whole page. Sections may pick from it; none
       may invent a value, or eleven bands drift out of rhythm with each
       other. */
    --sp-1: 8px;
    --sp-2: 14px;
    --sp-3: 22px;
    --sp-4: 34px;
    --sp-5: 48px;
    --sp-6: 64px;

    --section-y: var(--sp-5);
    --radius: 12px;
    /* Line length, not element width: past roughly 34em the eye loses the
       start of the next line. */
    --measure: 34em;

    --fs-h2:    clamp(24px, 5.4vw, 34px);
    --fs-h3:    clamp(18px, 4.2vw, 21px);
    --fs-lead:  clamp(16.5px, 4.1vw, 18px);
    --fs-small: 14px;

    /* MOTION SCALE. Two curves and three durations, and nothing on this page
       is allowed a fourth. The page previously had seven transitions across
       five durations, six of them on the browser default `ease` — which is
       what unconsidered motion looks like: every element moving to its own
       clock, so the page feels assembled rather than designed.

       --ease-out is an expo-out: it commits immediately and decelerates into
       place. It is for things ARRIVING and settling — a drawer, a bar, the
       rotating word — because that shape reads as confident.
       --ease-std is symmetric, for state that merely CHANGES rather than
       travels: colour on hover, a border appearing.

       Durations: t-1 for colour and small state, t-2 for anything that moves
       across the screen. The word's dissolve is longer on purpose — it is
       the one motion a visitor is meant to actually watch. */
    --ease-out: cubic-bezier(.16, 1, .3, 1);
    --ease-std: cubic-bezier(.4, 0, .2, 1);
    --t-1: .18s;
    --t-2: .3s;

    /* The dissolve, in four numbers.
         dis-dur   how long ONE character takes to break up or settle
         dis-step  the gap between consecutive characters
         dis-gap   when the new word starts arriving, measured from the
                   moment the old one starts leaving. Longer than dis-dur so
                   the halves are sequential — the old word is most of the way
                   gone before the new one appears — but short of the full
                   exit (.3 + 9 steps = .516s for the longest word) so the two
                   overlap by about a tenth of a second and there is no dead
                   beat in the middle.
         dis-width the box resize, which is delayed by dis-dur so it happens
                   while the slot is emptiest and nobody is reading it. */
    --dis-dur: .3s;
    --dis-step: 24ms;
    --dis-gap: .42s;
    --dis-width: .5s;

    /* GLASS. One recipe, used by both floating surfaces on the page — the
       header and the fixed CTA bar — so they read as two panes of the same
       material rather than two different effects.

       What makes it read as glass rather than "a blurred rectangle":

       1. The tint is a GRADIENT, not a flat fill, and it is deliberately
          thin. The previous version was rgba(255,255,255,.92): at 92% the
          blur behind it was doing almost nothing you could see. Glass you
          cannot see through is just paint. Top is denser than bottom, which
          is how a pane lit from above actually looks — and it also keeps the
          density highest exactly where the nav text sits.
       2. A LIT BEVEL along the top edge (--glass-bevel, an inset highlight).
          This is the single detail that sells thickness; a pane with no edge
          highlight reads as flat.
       3. The blur is paired with SATURATE. Real glass concentrates the colour
          it transmits. On a monochrome page this does nothing at all to the
          greys — it only shows on the photographs and the video thumbnail
          passing underneath, which is exactly where you want it.

       What it deliberately does NOT have is anything cast BELOW the pane.
       A drop shadow and a soft falloff were tried and removed: over light
       content they read as a bright haze bleeding onto the page rather than
       as glass. The pane ends at its own hairline, and every edge detail
       sits on the glass instead of under it. */
    --glass-blur: 22px;
    --glass-sat: 165%;
    --glass-tint-a: rgba(255, 255, 255, .72);
    --glass-tint-b: rgba(255, 255, 255, .52);
    --glass-bevel: rgba(255, 255, 255, .9);
    --glass-line: rgba(23, 23, 23, .1);
    /* The opaque stand-in where backdrop-filter is unsupported. A token
       because the dark scheme has to override it too: left hardcoded, an
       old browser in dark mode would get a white bar over a black page. */
    --glass-solid-a: rgba(255, 255, 255, .97);
    --glass-solid-b: rgba(255, 255, 255, .94);

    /* Tells the browser which schemes this page genuinely supports, so form
       controls, scrollbars and the canvas behind the page match. Without it
       a dark-mode browser paints a white canvas behind a dark page and
       flashes it on every load. */
    color-scheme: light dark;
}

/* ── Dark theme ─────────────────────────────────────────────────────────── */
/* Follows the DEVICE setting, with no toggle and no JS: prefers-color-scheme
   is the OS/browser preference, so the page simply arrives in the scheme the
   visitor already chose everywhere else.

   Only ROLES are redefined. The palette above is untouched, because a token
   called --white must never stop being white.

   CONTRAST, measured rather than eyeballed, all against --bg #0E0E0E:
     --ink   #F2F2F2  16.9:1   body and headings
     --muted #A8A8A8   8.0:1   secondary text (5.28:1 in light, so dark is
                               the more legible of the two schemes)
     --faint #6E6E6E   3.7:1   decoration only, clears the 3:1 non-text bar
     --jade  #00BB77   7.6:1   and this is the real prize: jade is 2.51:1 on
                               white and FAILS AA there, but on this ground
                               it passes comfortably. The stat figures, which
                               are the one place jade carries information
                               rather than decorating it, are legible in dark
                               mode in a way they are not in light. */
@media (prefers-color-scheme: dark) {
    :root {
        --bg:          #0E0E0E;
        --surface:     #1A1A1A;
        --card-bg:     #1A1A1A;   /* raised above --bg, the dark-UI way of
                                     separating a card: a lighter surface,
                                     not a shadow, because a black shadow on
                                     a black page is invisible. */
        --ink:         #F2F2F2;
        --muted:       #A8A8A8;
        --story-fade:  14, 14, 14;
        --faint:       #6E6E6E;
        --line:        #2A2A2A;
        --solid:       #F2F2F2;
        --solid-hover: #FFFFFF;
        --on-solid:    #0E0E0E;
        --scrim:       rgba(0, 0, 0, .66);
        /* Still a shadow, and still doing very little: on a dark ground the
           --card-bg step above is what actually lifts the card. This only
           softens its bottom edge. */
        --card-shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 8px 20px -10px rgba(0, 0, 0, .9);

        /* Glass, inverted. The BEVEL stays a white highlight in both
           schemes, because a lit top edge is light by definition: that one
           line is what sells thickness, and a dark bevel on dark glass
           reads as a smudge. */
        --glass-tint-a: rgba(18, 18, 18, .76);
        --glass-tint-b: rgba(18, 18, 18, .56);
        --glass-bevel:  rgba(255, 255, 255, .12);
        --glass-line:   rgba(255, 255, 255, .10);
        --glass-solid-a: rgba(14, 14, 14, .97);
        --glass-solid-b: rgba(14, 14, 14, .94);
    }
}

/* CONTRAST RULES, written down because they are easy to break by eye:
     --grey-900 on white  16.8:1  body and headings
     --grey-600 on white   5.28:1 the ONLY legal secondary text colour
     --grey-400 on white   2.85:1 hairlines and decoration, NEVER text
     --grey-200            1.3:1  fills and dividers only
   A control's boundary needs 3:1 under WCAG 1.4.11, so a ghost button's
   border is --grey-900 and never --grey-200, however much lighter that
   would look. */

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

/* The `hidden` attribute is only `display: none` in the UA stylesheet, so ANY
   author display declaration silently beats it — `.stat { display: flex }`
   did exactly that and left a hidden cell rendering in full. Every element on
   this page that JS hides does so with the attribute, so the guard belongs
   here once rather than as a `[hidden]` override per component.
   !important is load-bearing, not lazy: the rule has to outrank whatever
   display a component sets on itself. */
[hidden] { display: none !important; }

html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
    scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--ink);
    font-family: var(--font);
    font-size: 16px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    overflow-x: hidden;
}

img, svg { max-width: 100%; display: block; }
a { color: inherit; }

/* Form controls do NOT inherit font-family from the page; left alone they
   render in the UA default, which on this machine is Arial. That is a second
   typeface on a page whose first rule is that there is only one, and it is
   invisible until something with text goes inside one. Fixed once, here,
   rather than per-component. */
button, input, select, textarea { font: inherit; color: inherit; }

.wrap {
    width: 100%;
    max-width: var(--wrap);
    margin-inline: auto;
    padding-inline: var(--wrap-pad);
}

.sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}
.skip:focus {
    position: fixed;
    top: 12px; left: 12px;
    width: auto; height: auto;
    clip: auto;
    z-index: 100;
    padding: 10px 16px;
    background: var(--ink);
    color: var(--on-solid);
    border-radius: 8px;
    font-weight: 600;
}

:where(a, button):focus-visible {
    outline: 2px solid var(--ink);
    outline-offset: 3px;
    border-radius: 4px;
}

/* ── Header ─────────────────────────────────────────────────────────────── */
/* FIXED, and that choice does two jobs. Out of flow, so it costs the hero no
   height and the heading still centres in the whole viewport. `sticky` could
   not do this anyway: body carries overflow-x: hidden, which makes body the
   scrollport, so a sticky header would stick to a box that scrolls with the
   page and never appear to work. And staying put means the menu is reachable
   from anywhere on a 7,800px page rather than only from the very top.
   Transparent over the hero, gaining a surface once the page has moved, so
   it never draws a bar across an otherwise empty hero. */
.site-header {
    position: fixed;
    inset: 0 0 auto;
    /* ABOVE the drawer (90), not below it. At z-index 70 the open drawer
       covered the right of the bar, which is exactly where the menu button
       is, so the close control was hidden behind the panel it closes. */
    z-index: 100;
}

/* THE PANE ITSELF, as a pseudo-element rather than the header's own
   background. It has to be a separate layer because it fades in and out, and
   fading the header would take the logo and the nav links with it. z-index:-1
   puts it behind the bar's own content: negative-z children paint below
   in-flow content, so the pane sits under the logo while still being inside
   the header's stacking context.

   Declared here, not added on .is-stuck, so backdrop-filter exists from first
   paint. Adding the property mid-scroll promotes the element to its own
   compositing layer at that moment, which shows up as a hitch on the first
   scroll; toggling only opacity on a layer that already exists does not. */
.site-header::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background-image: linear-gradient(to bottom, var(--glass-tint-a), var(--glass-tint-b));
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    border-bottom: 1px solid var(--glass-line);
    /* The lit bevel along the top edge, and NOTHING BELOW THE BAR. Both the
       drop shadow and the soft gradient falloff that used to hang under the
       pane are gone: they cast a bright haze onto the content underneath,
       which read as a glow rather than as glass. The pane now ends at its own
       hairline. What is left — the bevel and that hairline — sits ON the
       glass rather than under it, so the edge still has thickness without
       anything spilling onto the page. */
    box-shadow: inset 0 1px 0 var(--glass-bevel);
    /* Nothing to refract over an empty white hero, so the pane is genuinely
       absent there rather than drawing a bar across it. */
    opacity: 0;
    transition: opacity var(--t-1) var(--ease-std);
}
.site-header.is-stuck::before { opacity: 1; }

/* No backdrop-filter, no glass: fall back to a near-opaque pane so the nav
   text keeps its contrast instead of sitting on a 60%-transparent white
   sheet over whatever happens to be scrolling underneath. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .site-header::before,
    .cta-bar {
        background-image: linear-gradient(to bottom,
                          var(--glass-solid-a), var(--glass-solid-b));
    }
}
.site-header__bar {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    min-height: 68px;
}
/* Pushes everything after the logo to the right edge. */
.site-header__spacer { flex: 1; }

.site-header__link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 var(--sp-2);
    font-size: 15px;
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
}
.site-header__link:hover { text-decoration: underline; text-underline-offset: 4px; }

/* ── Menu button ────────────────────────────────────────────────────────── */
.menu-btn {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    gap: 5px;
    width: 44px;
    height: 44px;
    padding: 0 10px;
    border: 0;
    background: none;
    cursor: pointer;
}
.menu-btn__line {
    display: block;
    width: 100%;
    height: 2px;
    border-radius: 2px;
    background: var(--ink);
    transition: transform var(--t-2) var(--ease-out),
                opacity var(--t-1) var(--ease-std);
}
/* The three lines become a cross while the drawer is open, so the control
   reads as the same object in both states rather than as a second button. */
.menu-btn[aria-expanded="true"] .menu-btn__line:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.menu-btn[aria-expanded="true"] .menu-btn__line:nth-child(2) { opacity: 0; }
.menu-btn[aria-expanded="true"] .menu-btn__line:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* ── Drawer ─────────────────────────────────────────────────────────────── */
/* pointer-events rather than visibility: an opacity:0 overlay still swallows
   every click on the page underneath it, and pointer-events needs no
   transition timing to be correct. */
.scrim {
    position: fixed;
    inset: 0;
    z-index: 80;
    background: var(--scrim);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--t-2) var(--ease-std);
}
.scrim.is-open { opacity: 1; pointer-events: auto; }

.drawer {
    position: fixed;
    inset: 0 0 0 auto;
    z-index: 90;
    width: min(320px, 84vw);
    padding: calc(68px + var(--sp-4)) var(--sp-4) var(--sp-4);
    background: var(--bg);
    border-left: 1px solid var(--line);
    /* Parked off-screen with a transform rather than removed from the DOM, so
       opening is one composited transform instead of a layout pass.
       NO visibility toggle. It was here to keep the links out of the tab
       order, but `inert` already does that properly, and the transition on
       visibility meant the panel was still computed hidden for a frame after
       opening, so focusing the first link silently did nothing: a hidden
       element cannot take focus. Removing it fixes the cause rather than
       delaying the symptom. */
    transform: translateX(100%);
    transition: transform var(--t-2) var(--ease-out);
    overflow-y: auto;
    overscroll-behavior: contain;
}
.drawer.is-open { transform: none; }

.drawer__list { margin: 0; padding: 0; list-style: none; }
.drawer__item + .drawer__item { border-top: 1px solid var(--line); }
.drawer__link {
    display: block;
    padding: var(--sp-3) 0;
    font-size: var(--fs-h3);
    font-weight: 600;
    letter-spacing: -.01em;
    text-decoration: none;
}
.drawer__link:hover { text-decoration: underline; text-underline-offset: 4px; }
/* The mark stays 22px; the LINK grows to a 44px hit area around it. Padding
   rather than a min-height, so the box grows symmetrically about the logo and
   the bar's flex centring is unaffected. Negative inline margin pulls the
   enlarged box back so the logo still sits flush with the gutter. */
.site-header__logo {
    display: inline-flex;
    align-items: center;
    padding: 11px 12px;
    margin-inline: -12px;
}
.site-header__logo picture { display: flex; }
.site-header__logo img { height: 22px; width: auto; }

/* ── Hero ───────────────────────────────────────────────────────────────── */
/* One heading, centred on the screen. svh rather than vh because on a phone
   vh measures the viewport WITHOUT the address bar, which pushes a centred
   block below the fold until the user scrolls. */
/* Dead centre of the viewport, not "centre-ish once the padding is
   subtracted". The previous version had 96px top / 56px bottom padding,
   which is asymmetric, so place-items: center was centring the heading
   inside an already lopsided box — it sat visibly above true centre. No
   padding-block at all, on either breakpoint: the header floats over the
   hero (position: absolute, out of flow) so it costs the hero nothing, and
   .wrap's own inline padding is untouched. */
/* 92svh, not 100: a full-viewport heading with no subline, no button and
   nothing peeking below gives a visitor no reason to believe the page
   continues. Eight percent is enough for the next section's top edge to show
   without touching anything inside the hero. */
.hero {
    min-height: 92vh;
    min-height: 92svh;
    display: grid;
    /* TWO rows now — the wrap, then the full-width marquee — so this is
       align-CONTENT, which centres the pair as a group, not align-items,
       which would centre each row inside its own track. */
    align-content: center;
    /* minmax(0, 1fr), and both halves are load-bearing.
       Under the old `place-items: center` the wrap was shrink-to-fit, so it
       sized to its content's intrinsic width — which, once the marquee went
       in, meant the track's ~4,500px max-content, clamped by max-width to
       1120px and overflowing every phone by 760px. `width: 100%` on the row
       does not save it: percentages resolve to auto during intrinsic sizing,
       so the overflow:hidden window still reports the whole track width
       upward.
       Switching to stretch alone did not fix it either, because the implicit
       grid COLUMN is `auto` and an auto track is itself sized from the item's
       max-content — the overflow simply moved up one level. Naming the column
       minmax(0, 1fr) gives it a definite size that cannot be pushed out by
       content, and the 0 minimum is what allows it to be smaller than that
       content rather than growing to fit it.
       The wrap's own max-width and auto margins go on centring the content
       exactly as before. */
    grid-template-columns: minmax(0, 1fr);
    justify-items: stretch;
}

/* The upward shift is GONE, and deliberately so. It existed when the hero
   held one heading and nothing else: a lone line of type sits better above
   true centre than on it, and a transform was the way to nudge it without
   disturbing the grid centring that had just been fixed.

   The hero now holds a heading, a subheading, a button and three rows of
   questions. That content fills the space the lift was compensating for, and
   the lift had become actively harmful — at -120px on an 800px-tall desktop
   window it pulled the top of the heading clean off the top of the viewport.
   It also could not survive the marquee moving outside .wrap: a transform on
   the wrap moves the wrap only, so the marquee would have stayed put and
   opened a gap the exact height of the shift.

   align-content: center on .hero now does the whole job, and because
   min-height (not height) is what sizes the section, content taller than
   92svh grows the section instead of overflowing it. Nothing can be clipped. */

.hero__title {
    /* ONE size for the whole heading; the three lines are ratios of it, so
       the relationship between them cannot drift when the scale is retuned.
       Declared on the h1 rather than per line precisely so `em` below means
       "a fraction of the heading", and so max-width is expressed in the
       heading's own units instead of a number that means something different
       at every breakpoint. */
    --hero-fs: clamp(30px, 9vw, 72px);
    font-size: var(--hero-fs);

    /* margin-inline: auto is load-bearing. text-align centres each LINE
       inside the box; the box itself still sits at the left of .wrap once
       max-width starts biting, which is every screen above roughly 700px.
       14em, not a ch count: ch is measured off the digit zero at the
       *inherited* size, which was 16px body text here, so 22ch was a 210px
       box that the (much larger) lines simply overflowed. */
    margin: 0 auto;
    max-width: 14em;
    text-align: center;
    letter-spacing: -.02em;
    color: var(--ink);
}

/* The subheading: one line of the secondary voice under the heading, in the
   same grey-600/fs-lead vocabulary .section__lead uses everywhere else on
   the page, just centred to match the hero rather than left-aligned like a
   body paragraph. Kept to its own measure rather than the heading's 14em —
   at hero width a 31-word sentence would run nearly edge to edge and lose
   the "short, considered statement" quality a subheading needs. */
.hero__sub {
    margin: var(--sp-4) auto 0;
    max-width: 30em;
    font-size: var(--fs-lead);
    font-weight: 400;
    line-height: 1.55;
    color: var(--muted);
    /* Not inherited from .hero__title — that centring is scoped to the h1,
       and a plain <p> sibling defaults back to the page's left alignment. */
    text-align: center;
}

/* The hero's CTA reuses .btn/.btn--solid — the same control the rest of the
   page books a Growth Review with — rather than inventing a second button
   style for the one place a visitor sees first. What is hero-specific is
   only the centring: the rest of the page is left-aligned copy, and
   dropping a centred hero button straight onto .btn unchanged would have
   left it pinned to the left edge under a centred heading. This wrapper
   does the one thing differently — centres its child. */
.hero__cta {
    display: flex;
    justify-content: center;
    margin-top: var(--sp-4);
}

/* ── The question marquee ───────────────────────────────────────────────── */
/* Ported from the customer mobile app's guest pre-chat hero
   (mobile/src/screens/DashboardScreen.jsx — MarqueeRow + its styles), kept
   deliberately faithful: same three rows, same 10px gap used BOTH between
   chips and between copies, same chip geometry (8px/16px padding, 18px
   radius). Only the palette and type are re-pointed at this site's tokens,
   because the app's #f7f7f7 / #8e8e93 chip is a hardcoded pair this page has
   its own names for — and #8e8e93 would have failed the contrast rule at the
   top of this file. --grey-600 on --grey-100 measures 4.84:1. */

/* Edge to edge, and it gets there by SITTING OUTSIDE .wrap rather than by
   fighting its way back out with negative margins. An infinite marquee only
   reads as endless if it runs off both sides; inside .wrap it would have
   been capped at the 1120px measure, leaving a white gutter on a desktop and
   turning it into a bounded box with things sliding inside it. As a direct
   child of .hero — which spans the viewport — it is full width by
   construction, with no negative margins to keep in step with --wrap-pad and
   no 100vw, which includes the scrollbar and has caused horizontal overflow
   on this page before. */
.hero__marquee {
    margin-top: var(--sp-5);
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* The window onto the track. The scrolling itself is entirely the track's
   transform; this is only what clips it. */
.marquee {
    overflow: hidden;
    width: 100%;
}

/* max-content so the track is exactly as wide as however many copies the
   script decided it needs. The gap here sits BETWEEN copies and must match
   the group's own internal gap, or the seam where one copy ends and the next
   begins is spaced differently from every other chip gap and gives away the
   loop point. */
.marquee__track {
    display: flex;
    width: max-content;
    gap: 10px;
    will-change: transform;
}
.marquee__group {
    display: flex;
    gap: 10px;
}
.marquee__chip {
    flex-shrink: 0;
    white-space: nowrap;
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 8px 16px;
    border-radius: 18px;
    background: var(--surface);
    color: var(--muted);
    font-size: var(--fs-small);
    font-weight: 500;
}

/* Only some chips carry one, so the padding stays the same on all of them
   and the icon lives inside the existing box rather than widening it into a
   different shape. currentColor, like every other icon on the page: the mark
   is the chip's own text, drawn, and it must never drift away from it. 15px
   against 14px type, so it reads level with the cap height instead of
   looming over it. */
.marquee__ico {
    flex-shrink: 0;
    width: 15px;
    height: 15px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* The <symbol> definitions the chips point at. Takes no space and is never
   painted, but stays in the layout tree rather than display:none, because a
   display:none <svg> makes its symbols unreachable in some engines. */
.sprite {
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
}

/* Two deliberate WEIGHTS at nearly one size, not three sizes. The rotating
   word is still the loudest thing in the hero — it is the answer to "is this
   page about me" — but it now carries that on weight and colour rather than
   on being bigger, so the top two lines read as one continuous sentence in
   one block instead of a caption sitting over a headline. */
/* Block, not inline-block. Each line then owns its own leading outright; as
   inline-block they sat in line boxes struck to the h1's line-height, which
   no per-line value can shrink, and the heading measured 346px tall where its
   three lines need 214. */
.hero__line { display: block; }

.hero__line--lead,
.hero__line--tail {
    font-weight: 300;
    line-height: 1.15;
}

/* 10% below the bold line below it, rather than the exact match this was
   briefly set to — at 1em the two lines were the same size and the eye had
   only weight to tell them apart, which read as too subtle a difference for
   two lines this prominent. .9em keeps them close enough to read as one
   sentence broken across two lines by choice, while size does some of the
   work weight was doing alone. */
.hero__line--lead { font-size: .9em; }

/* The close is sized to HOLD ONE LINE, which is the only thing deciding this
   number — bumped up from .5em/4.3vw, its size before this pass, as far as
   the line still fits at one line with "how to Grow it" bold. "and figure
   out how to Grow it." is 27 characters and part of it is bold, and
   Montserrat's bold glyphs run wider, so the ceiling is tighter than the
   character count alone would suggest. Two caps, whichever is smaller wins:
   .58em keeps it proportional to the heading on a desktop, 4.5vw is what
   actually holds the line on the narrowest phones, where .58em of a
   clamped-up heading would be too big. */
.hero__line--tail {
    font-size: min(.58em, 4.5vw);
    /* A small gap of its own, separate from the bold line's leading. Without
       it "your business" and the close sat only as far apart as
       line-height put them, which read as cramped now that the close is a
       full sentence rather than a short caption. In em of the CLOSE's own
       (smaller) size, not the heading's, so the gap stays proportional to
       the text it separates rather than to the bold word above it. */
    margin-top: .55em;
    color: var(--muted);
}

/* "how to Grow it" carries the weight of the line — it is the answer, "and
   figure out" is the setup. The line stays grey-600 apart from the single
   green word: two weights and one accent inside a phrase this short is
   already the limit, and darkening the rest as well would read as three
   voices competing. (Its width is already accounted for in the caps above.) */
.hero__emph { font-weight: 700; }

/* The only coloured word on the page. It is the fifth rung of the spine —
   Understand, Decide, Execute, Improve, GROW — landing in the h1 as the
   thing all of it is for, which is why exactly one word gets the colour and
   nothing else competes for it. Inherits weight from .hero__emph, so the
   colour is the only difference; see --jade for why it must never carry
   meaning on its own. */
.hero__grow { color: var(--jade); }

/* "your" sits on the bold line at the SAME size, differing only in weight.
   Same size is load-bearing, not cosmetic: .rot has overflow:hidden, which
   makes its baseline its bottom edge, so it is bottom-aligned rather than
   baseline-aligned. Bottom alignment only coincides with a shared baseline
   while both sides have identical font metrics and line-height — at two
   different sizes the descenders differ and the word sits visibly off. */
.hero__line--word {
    margin-block: .04em;
    font-size: 1em;
    font-weight: 700;
    /* 1.25, not 1.05: this is the height of the mask the word rolls through,
       and Montserrat's ascender plus descender is 1.219em. Any tighter and
       the p in "profession" is clipped while sitting still. */
    line-height: 1.25;
}
.hero__your { font-weight: 300; }

/* ── Rotating word ──────────────────────────────────────────────────────── */
/* One word in the heading cycles through business / profession / idea, which
   is how the hero addresses all three audiences in a single sentence.

   A DISSOLVE, per character. The old word breaks up and clears the slot,
   then the new one settles into the space it left.

   Blur is what makes it a dissolve rather than a fade. Opacity alone just
   turns text grey on its way out — the letterforms stay sharp and legible
   until the moment they vanish, which is why a plain fade reads as cheap.
   Blurring as it goes means the word stops being readable BEFORE it stops
   being visible, so it genuinely disperses. The radius is in em, not px, so
   it is the same dissolve at 30px on a phone as at 72px on a desktop; a
   fixed px radius would be a haze on one and a smudge on the other.

   Per character, staggered, because a word that dissolves all at once is a
   fade with extra steps. Letter by letter it reads as the word coming apart
   and the next one assembling — the thing worth watching in the hero.

   The two halves are SEQUENTIAL, which is the whole brief: the old word goes
   first, and only then does the new one arrive to take its place. They share
   a small overlap (--dis-gap lands while the last few letters are still
   clearing) purely so there is no dead beat between them. */
.rot {
    display: inline-block;
    position: relative;   /* the outgoing word is absolute inside this */
    /* No overflow:hidden — the blur has to spill past the text box or it is
       cut into a hard-edged rectangle, which is the one thing that would give
       the effect away. Nothing needs clipping now that nothing slides. */
    transition: width var(--dis-width) var(--ease-out) var(--dis-dur);
    will-change: width;
}
.rot__word {
    display: block;
    white-space: nowrap;
}

/* The outgoing word is a clone, lifted out of flow so the incoming one can
   take the slot without waiting for it to be removed. */
.rot__word--clone {
    position: absolute;
    inset: 0 0 auto;
    width: 100%;
}

.rot__char {
    display: inline-block;
    white-space: pre;
    /* --i is the character's index, set in JS; --d is its delay. The default
       here is the ARRIVING delay, because the live word is the one that
       animates in. The clone overrides it below to leave immediately. */
    --d: calc(var(--dis-gap) + var(--i) * var(--dis-step));
    transition: opacity var(--dis-dur) var(--ease-std) var(--d),
                filter var(--dis-dur) var(--ease-std) var(--d),
                transform var(--dis-dur) var(--ease-std) var(--d);
}

/* Dissolved. Both directions land on the same state, so the word leaves the
   way the next one arrives — the drift is opposite in sign, which is what
   stops it looking like the same letter bouncing. */
.rot__word--out .rot__char {
    --d: calc(var(--i) * var(--dis-step));
    opacity: 0;
    filter: blur(.16em);
    transform: translateY(-.08em) scale(.94);
}
.rot__word--enter .rot__char {
    opacity: 0;
    filter: blur(.16em);
    transform: translateY(.08em) scale(.94);
    /* Applied with transitions off: this is the START of the arrival, not
       something to animate to. Without this the new word would first
       dissolve itself and then reassemble. */
    transition: none;
}

/* Reduced motion: no dissolve, no width animation. The word still changes,
   it just does so instantly, which is the point of the setting. */
@media (prefers-reduced-motion: reduce) {
    .rot, .rot__char { transition: none; }
}

/* Mobile gets a FIXED gap under the header rather than the viewport-relative
   shift .hero .wrap uses above 639px. The two approaches don't mix: an
   svh-based transform gives a different number on every phone, and "120px
   below the header" was asked for as an exact one. This block MUST come
   after the .hero .wrap rule above — both rules target the same selector at
   equal specificity, so whichever is later in the file wins once its media
   query matches, and this one had originally been placed before it by
   mistake, which silently overrode `transform: none` right back to the
   svh-based value on every phone. */
@media (max-width: 639px) {
    /* align-CONTENT, matching the base rule: the hero is a two-row grid
       (wrap, then marquee) and this pins that pair to the top rather than
       centring each row in its own track. */
    .hero { align-content: start; }
    .hero .wrap {
        /* calc(68px + 80px): the header's own height (site-header__bar,
           declared above) plus 80px of clear space under it (120px, then
           moved 40px closer to the header). Written as the sum rather than
           the literal 148 so the intent survives if the header height ever
           changes. Padding on the wrap rather than the hero, so the marquee
           below is not pushed down by it too. */
        padding-top: calc(68px + 80px);
    }
}

/* ── Section shell ──────────────────────────────────────────────────────── */
/* Every band below the hero is a .section. TWO WEIGHTS, deliberately: heavy
   sections carry an eyebrow, a large title and a lead; standard ones drop
   the lead. A page this short reading as a corridor of identical weight is
   less of a risk than it was at twelve sections, but the distinction still
   earns its keep. --tint and --tight (background tint, shrunk padding) were
   used by sections that no longer exist and are removed rather than kept
   for a hypothetical future one — see .section--flush-top below for the
   modifier that replaced the job --flush used to do alone. */
.section { padding-block: var(--section-y); }
.section--rule   { border-top: 1px solid var(--line); }
.section--flush  { padding-block: 0; }
/* Only the top: for a section that opens with full-bleed media rather than
   text, so the media sits flush against the hairline above it instead of
   floating --section-y below it. Bottom padding is untouched — the section
   still ends with the usual breathing room before whatever comes next. */
.section--flush-top { padding-top: 0; }

/* Anchor targets must clear the FIXED header, or a jump lands with the
   section's heading tucked underneath it. Header height plus a breath. */
.section[id] { scroll-margin-top: calc(68px + var(--sp-3)); }

/* ── Section-opening photo (.lede) ───────────────────────────────────────
   Used by #what-we-do and #why-us. The whole photograph, plainly:

   1. EDGE TO EDGE. The figure sits outside .wrap, and .section has no
      horizontal padding, so width:100% already is the viewport. No 100vw:
      that counts the scrollbar and overflows the page, which this stylesheet
      has been bitten by before (see .chain, .bene).
   2. NO CROP, NO ZOOM. height:auto on the image, so every photo shows at its
      own natural height for the width it's given — nothing above or below
      it is ever cut off.
   3. NO OVERLAY. There used to be a fade drawn over the bottom of the photo,
      meant to blend it into the page instead of ending on a hard edge. On
      some screens the fade itself left a visible seam, which read worse
      than the edge it was covering, so it is gone: what follows is simply
      the photo, then the heading at the section's ordinary spacing. */
.lede {
    position: relative;
    margin: 0;
}
/* The founder photo comes through a <picture> (one file per theme). Inline by
   default, which would leave the figure a line box taller than the image. */
.lede picture { display: block; }
.lede__img {
    display: block;
    width: 100%;
    /* The whole photograph, uncropped and unzoomed: full width, and whatever
       height that width makes it. The width/height attributes on the <img>
       hold the 16:9 box before it loads, so nothing below it jumps. */
    height: auto;
}
/* Ordinary spacing below the photo — the same gap a section's own top
   padding would give its first heading, restored here because
   .section--flush-top removed it to let the photo itself sit flush against
   the hairline above. */
.wrap--lede {
    margin-top: var(--section-y);
}
/* The founder's line (#why-us). Above the photo by default, which is where
   the section lead always sat, so wide screens look exactly as before. */
.lede__quote {
    margin-block: 0;
}
/* On a phone it is set INTO the photo: bottom left, 60% of the width. Its
   left edge is the page gutter, the same as the heading above, so the two
   still line up.

   Read the photograph before changing these numbers. The left 35% is empty
   wall at every height, and everything above half the height is empty out
   to about 60%. Below that the chair and his arm come in: measured on the
   source file, his hand sits between 40% and 64% across, from 65% to 92%
   down. So at 60% wide the last lines do cross his arm. That is the width
   that was asked for; the narrower 42% column that cleared him entirely is
   in git history if it is ever wanted back.

   The size is driven by the box, not by taste: a full sentence set small
   and light, a step down from the version that sits above the photo on
   wider screens. text-wrap is left alone here, because balance evens the
   lines out and pushes the longest one further right, deeper into him.

   No overlay and no crop: the text sits on the wall in a photo that is
   already flattened onto each theme's background, so --ink reads on both. */
@media (max-width: 639px) {
    .lede { margin-top: var(--sp-2); }
    .lede__quote {
        position: absolute;
        z-index: 1;
        left: 0;
        bottom: 0;
        width: 60%;
        padding-right: 0;
        padding-bottom: var(--sp-2);
    }
    .lede__quote .section__lead {
        margin: 0;
        font-size: clamp(11.5px, 3.5vw, 15px);
        line-height: 1.4;
        font-style: italic;
        font-weight: 500;
        color: var(--ink);
    }
    /* An opening mark, so it reads as said by him rather than as a caption.
       Decoration only, hence --faint, and hidden from screen readers by
       being generated content on a <blockquote> that already says "quote". */
    .lede__quote .section__lead::before {
        content: "\201C";
        display: block;
        height: .45em;
        margin-bottom: .1em;
        font-size: 2.2em;
        line-height: 1;
        font-style: normal;
        color: var(--faint);
    }
}
/* Whatever sits first inside it brings no top margin of its own, or the pull
   above is spent paying for it: the founder block's own --sp-4 put his
   message 30px BELOW the photo it is supposed to begin inside. */
.wrap--lede > :first-child { margin-top: 0; }

.section__eyebrow {
    margin: 0 0 var(--sp-2);
    font-size: 13px;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
}
.section__title {
    margin: 0;
    max-width: 18ch;
    font-size: var(--fs-h2);
    font-weight: 700;
    line-height: 1.14;
    letter-spacing: -.015em;
    text-wrap: balance;
}
.section__lead {
    margin: var(--sp-3) 0 0;
    max-width: var(--measure);
    font-size: var(--fs-lead);
    line-height: 1.6;
    color: var(--muted);
    /* Matches .bene__text: a one-word final line ("up.") is a runt, and it
       looks like broken spacing rather than prose. This was missing on the
       shared primitive itself — every section using .section__lead was
       exposed to it before a since-removed section's longer paragraph
       happened to be the one that landed on a runt first. */
    text-wrap: pretty;
}
/* Reassurance lines, captions, small print. Never --grey-400: it fails. */
.section__note {
    margin: var(--sp-2) 0 0;
    font-size: var(--fs-small);
    line-height: 1.55;
    color: var(--muted);
}

/* ── Callout card ───────────────────────────────────────────────────────── */
/* Sets a line apart with a fill instead of a border — no line to draw, no
   shadow to fake depth with, just --grey-100 and a radius. Client's own
   spec: "simple, no borders, only rounded cornered a very light greyed
   card." --grey-600 on --grey-100 is 4.84:1 (see the note above .marquee),
   comfortably past the 4.5:1 this size needs. */
.callout {
    margin: var(--sp-3) 0 0;
    max-width: var(--measure);
    padding: var(--sp-3);
    background: var(--surface);
    border-radius: var(--radius);
    color: var(--muted);
    line-height: 1.6;
    text-wrap: pretty;
}

/* ── Stack ──────────────────────────────────────────────────────────────── */
/* Vertical flow with one knob. Beats margins on children because the gap
   belongs to the container, so a section can change its own rhythm without
   every child needing a modifier. */
.stack { display: flex; flex-direction: column; gap: var(--stack-gap, var(--sp-3)); }
.stack--sm { --stack-gap: var(--sp-2); }
.stack--lg { --stack-gap: var(--sp-5); }

/* ── Paired headings ────────────────────────────────────────────────────── */
/* "What we do" and "How it helps you" as two headings with a paragraph each,
   instead of one long title and one lead. The section asks two questions, so it
   answers them in two places rather than running both into a single block the
   reader has to separate for themselves.

   Sized at --fs-h3 rather than --fs-h2: there are two of them, one after the
   other, and two full section titles stacked would fight each other and shout
   over the eyebrow above. The eyebrow carries the section's identity; these
   carry its two answers. */
.duo + .duo { margin-top: var(--sp-3); }
.duo:first-of-type { margin-top: var(--sp-3); }
.duo__h {
    margin: 0;
    font-size: var(--fs-h3);
    font-weight: 650;
    letter-spacing: -.01em;
    text-wrap: balance;
}

/* Standalone block headings — "Benefits", "What's the process". Deliberately
   the SAME type as .duo__h rather than something smaller: the section is four
   blocks now (what we do / how it helps you / benefits / the process), and they
   are peers, not a hierarchy. Sized down from these and they would read as
   captions belonging to the block above rather than as the label for the one
   below.

   NOT the eyebrow treatment either. Uppercase 13px grey is this page's idiom
   for a section's own identity line, and reusing it mid-section would put two
   things that look identical — "Understand the full concept" and "Benefits" —
   inside one band, saying different kinds of thing.

   --sp-5 above is what does the separating: with no rule and no fill on this
   page, space is the only divider available. The pair below cancels the top
   margin the list and the rail carry for the case where they follow a
   paragraph, so a heading plus its content reads as one block instead of two
   drifting apart. */
.subhead {
    margin: var(--sp-5) 0 var(--sp-2);
    font-size: var(--fs-h3);
    font-weight: 650;
    letter-spacing: -.01em;
    text-wrap: balance;
}
.subhead + .rows,
.subhead + .bene,
.subhead + .stepper { margin-top: 0; }

/* ── Benefit cards ──────────────────────────────────────────────────────── */
/* Icon over a bold title over one grey line, six across, scrolling sideways —
   built to a reference the client supplied. The reference's icons are
   coloured; these are --ink, which is both the brief and this page's own rule
   that nothing except photographs carries colour.

   Same scroll mechanics as .stepper, deliberately: full bleed by cancelling
   .wrap's gutter and adding it back as the rail's own padding, so the first
   card lines up with the heading while the last can clear the right edge, and
   cards run off both sides — which is what says "there is more". No 100vw; it
   includes the scrollbar and has overflowed this page before.

   STILL NO fills and NO drop shadow here. The page has exactly one drop
   shadow, on .story, and one inset bevel, on the fixed header/CTA bar's
   glass. Six decorative shadows on a scrolling rail would be something else
   again, on a page whose whole restraint is that nothing is decorated
   without a reason. Six bordered boxes
   would repeat the thing .rows already rejects in its own comment: cards cost
   real height/width for no gain in comprehension over hairlines.

   A THIN VERTICAL LINE instead — the horizontal-rail version of the hairline
   .rows already draws between vertically stacked items, so this reads as the
   same idiom turned 90°, not a new one. It is a ::before on every item except
   the first, not a `gap`-based border, so it never touches the measured
   157px card width or the "six fit in 1056px" arithmetic below — that sum is
   unchanged and does not need re-checking because of this. top:0/bottom:0
   works because .bene has no align-items override, so flex's default
   (stretch) already gives every card the same height as the tallest one; the
   line rides that height for free. */
.bene {
    margin: var(--sp-4) calc(var(--wrap-pad) * -1) 0;
    padding: 0 var(--wrap-pad);
    display: flex;
    gap: var(--sp-3);
    list-style: none;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scroll-padding-left: var(--wrap-pad);
    overscroll-behavior-x: contain;
    /* Both are needed: scrollbar-width covers Firefox and current Chrome, the
       ::-webkit- rule covers Safari, which does not support the standard one. */
    scrollbar-width: none;
}
.bene::-webkit-scrollbar { display: none; }
.bene:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

/* 157px is measured, not guessed, and the arithmetic is worth keeping because
   it is easy to break. The rail's usable width on a desktop is the wrap minus
   its gutters — 1120 - 64 = 1056 — and the bleed trick above does not change
   that, since the negative margin and the padding cancel out. Six cards plus
   five --sp-3 gaps must fit inside it:
       6w + 5(22) <= 1056   ->   w <= 157.6
   At the 168px this first shipped with, the sixth card was clipped by 62px on
   a 1280 screen: visible enough to look like a mistake rather than a hint that
   the rail scrolls. If the gap or the wrap ever changes, redo this sum.
   The percentage is a floor for very narrow screens, where a flat 157 would
   crowd the gutter. */
.bene__item {
    position: relative;
    flex: 0 0 min(62%, 157px);
    scroll-snap-align: start;
}
/* Centred in the gap rather than flush against either card, so it reads as a
   divider between two things rather than a border belonging to one of them.
   Half the gap out, minus half a pixel for the line's own width. */
.bene__item:not(:first-child)::before {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: calc(var(--sp-3) / -2 - .5px);
    width: 1px;
    background: var(--line);
}

/* Shared stroke attributes live here rather than being repeated on all six
   SVGs. currentColor is what keeps an icon from ever drifting away from the
   text it belongs to. */
.bene__icon {
    display: block;
    width: 28px; height: 28px;
    color: var(--ink);
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.bene__title {
    margin: var(--sp-2) 0 0;
    font-size: 16px;
    font-weight: 650;
    letter-spacing: -.01em;
    text-wrap: balance;
}
.bene__text {
    margin: 4px 0 0;
    font-size: var(--fs-small);
    line-height: 1.55;
    color: var(--muted);
    text-wrap: pretty;
}
.duo__p {
    margin: 6px 0 0;
    max-width: var(--measure);
    color: var(--muted);
    line-height: 1.6;
    text-wrap: pretty;
}

/* ── Growth numbers ─────────────────────────────────────── */
/* A 2x2 grid ruled by hairlines rather than boxed into cards: the rules do
   all the separating a grid this simple needs, and four bordered boxes
   would cost real height for no gain in comprehension.

   The figure is the whole point of the section, so it gets the largest type
   on the page after the h1 and the section titles, and everything else in
   the cell recedes to --fs-small.

   THE NUMERALS ARE --jade AT THE CLIENT'S INSTRUCTION, and this is the one
   place on the page where that colour carries information rather than
   decorating it. It is worth being clear about the cost: --jade is 2.51:1 on
   white, which fails WCAG AA for text at every size (4.5:1 normal, 3:1
   large), and unlike the h1's "Grow" — a word whose sentence reads
   identically without the colour — the figure here IS the content, so a
   reader who cannot resolve it loses the fact itself. The label beneath
   stays --grey-600 at 5.28:1, so the cell is never unreadable in full. If
   this is ever revisited, #009960 clears 3:1 at this size and #007A4D clears
   4.5:1, both the same hue. */
.stats {
    margin: var(--sp-4) 0 0;
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    border-top: 1px solid var(--line);
}
.stat {
    display: flex;
    /* Value above label, while the DOM keeps <dt> before <dd>. */
    flex-direction: column-reverse;
    align-items: flex-start;
    gap: 4px;
    padding: var(--sp-3) var(--sp-2) var(--sp-3) 0;
    border-bottom: 1px solid var(--line);
}
/* The vertical rule between the columns. :nth-child(odd) is the left column,
   and the padding above keeps the right column's text off it. */
.stat:nth-child(odd) {
    padding-right: var(--sp-3);
    border-right: 1px solid var(--line);
}
.stat:nth-child(even) { padding-left: var(--sp-3); }

.stat__value {
    margin: 0;
    color: var(--jade);
    font-size: clamp(34px, 8.5vw, 52px);
    font-weight: 700;
    line-height: 1;
    letter-spacing: -.035em;
    /* Tabular figures so the numerals share one advance width. Without it a
       repaint from 9 to 11 visibly shifts the label under it, and two cells
       side by side sit on different optical grids. */
    font-variant-numeric: tabular-nums;
}
.stat__label {
    font-size: var(--fs-small);
    font-weight: 500;
    line-height: 1.4;
    color: var(--muted);
    text-wrap: balance;
}

@media (min-width: 640px) {
    .stat { padding-block: var(--sp-4); }
}

/* ── Growth stories ─────────────────────────────────────── */
/* Built to a reference card the client supplied: avatar, name and role across
   the top, a big pale quote mark in the top right, and the quotation itself
   in italic underneath.

   THE PAGE'S FIRST DROP SHADOW, and a deliberate exception. Everything else
   here is separated by hairlines, and the only other box-shadows are the
   1px inset bevels on the glass header. This one is kept to the smallest
   thing that still reads as a lifted card: a 1px contact shadow plus a wide,
   very soft ambient one, both pure black at low alpha so no hue leaks in. If
   more cards ever appear elsewhere they should reuse this, not invent a
   second shadow. */
/* Full-bleed horizontal rail, the same construction as .chain and .bene:
   cancel .wrap's gutter with a negative margin, add it back as the rail's
   own padding, so the first/last card lines up with the rest of the page's
   text while the rail itself can scroll edge to edge. Never 100vw — that
   includes the scrollbar and causes horizontal page overflow. */
.stories {
    /* --story-shadow-room exists so the card's shadow (below, in .story) has
       somewhere to fall. overflow-x: auto forces this element's computed
       overflow-y to auto as well (that is how the overflow spec works — one
       axis scrollable, the other can't stay visible), which clips the
       shadow flush against the container's own box the instant it is set.
       Padding gives the shadow room to render before that clip line; the
       matching negative margin on top and bottom cancels the padding back
       out so the rail's position relative to the heading above and the
       section rule below is unchanged — only the shadow gets the extra
       space, nothing else moves. */
    --story-shadow-room: 18px;
    margin: calc(var(--sp-4) - var(--story-shadow-room)) calc(var(--wrap-pad) * -1) calc(var(--story-shadow-room) * -1);
    padding: var(--story-shadow-room) var(--wrap-pad);
    display: flex;
    gap: var(--sp-3);
    list-style: none;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scroll-padding-left: var(--wrap-pad);
    overscroll-behavior-x: contain;
    /* Both are needed: scrollbar-width covers Firefox and current Chrome, the
       ::-webkit- rule covers Safari, which does not support the standard one. */
    scrollbar-width: none;
}
.stories::-webkit-scrollbar { display: none; }
.stories:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

.stories > li {
    flex: 0 0 min(86%, 400px);
    scroll-snap-align: start;
}

.story {
    /* No max-width here any more: .stories > li's flex-basis is what sizes
       the card now (min(86%, 400px)), so a second cap on .story itself would
       just be dead weight. */
    /* 5:4, the same shape the review photo is cropped to, so a photo card
       shows the whole image with nothing trimmed. Fixed rather than
       content-driven: long quotes are clamped (see .story__quote) instead of
       stretching the card. */
    aspect-ratio: 5 / 4;
    position: relative;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: var(--sp-2);
    background: var(--card-bg);
    border: 1px solid var(--line);
    border-radius: 18px;
    /* A tight 1px contact line plus a short, fairly dark ambient shadow reads
       as a crisp, deliberately lifted card. The earlier version spread the
       ambient shadow 28px at low alpha, which put a wide, pale haze under and
       past the card's edges on all sides — soft in a way that read as smudged
       rather than designed. This is the same two-layer idea, just tightened. */
    box-shadow: var(--card-shadow);
}

/* position: relative so the quote mark below can anchor to THIS row's
   height rather than to the card's. That is what keeps the mark reading as
   part of the header — vertically centred beside the avatar and name — no
   matter how long the quotation underneath grows. Pinned to the card's own
   top edge instead, it used to sit in open padding with nothing near it,
   which is what read as a sticker rather than a design element. */
.story__head {
    position: relative;
    display: flex;
    align-items: center;
    gap: 14px;
}

/* A neutral placeholder avatar, not a stand-in face. --grey-400 is banned for
   text but this is a decorative glyph, which is what that token is for. Swap
   the <svg> for an <img> or two initials when there is a real person here. */
.story__avatar {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 54px;
    height: 54px;
    border-radius: 50%;
    background: var(--surface);
    color: var(--faint);
}
.story__avatar svg { width: 28px; height: 28px; }
/* The generated photo fills the avatar slot. It was cropped 5:4, and this slot
   is a circle, so object-fit does the centre crop: a face centred in the crop
   survives it. overflow:hidden on .story__avatar is what clips it round. */
.story__avatar { overflow: hidden; }
.story__photo { width: 100%; height: 100%; object-fit: cover; display: block; }

/* A fixed 5:4 card cannot grow, so a long quote is clamped rather than
   spilling out of it. The full text is on the business's profile page. */
.story__quote p {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 4;
    overflow: hidden;
}

/* ── Photo card: the whole image is the background ──────────────────────
   The photo was cropped 5:4 and the card is 5:4, so object-fit: cover shows
   all of it with nothing trimmed. The text sits at the bottom over a dark
   gradient: without it, white type over a light photo is unreadable, and the
   gradient only darkens the lower part so the image above stays clear. */
.story--photo {
    justify-content: flex-end;
    border: 0;
    background: var(--surface);
}
.story__bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    z-index: 0;
}


.story--photo .story__name { color: var(--ink); }
.story--photo .story__role { color: var(--muted); }
.story--photo .story__quote,
.story--photo .story__quote p { color: var(--ink); }
.story--photo .story__quote p { -webkit-line-clamp: 4; }
.story--photo .story__head::after { display: none; }

.story__id { min-width: 0; }
/* <cite> is italic by default in every browser. The quotation is the italic
   thing on this card; the name is not. */
.story__name {
    display: block;
    font-style: normal;
    font-size: 18px;
    font-weight: 700;
    letter-spacing: -.01em;
    text-wrap: balance;
}
.story__role {
    display: block;
    margin-top: 3px;
    font-size: var(--fs-small);
    line-height: 1.45;
    color: var(--muted);
    text-wrap: pretty;
}

/* True italic, not a synthesised oblique: montserrat-latin-italic.woff2 is
   self-hosted alongside the roman, so font-style: italic resolves to a real
   drawn face. */
.story__quote {
    /* More than .story__head's own internal gaps: this is the seam between
       WHO is speaking and WHAT they said, and it needs to read as clearly as
       the seam in .story__by used to — the two are different kinds of
       information, not one continuous paragraph. */
    margin: var(--sp-3) 0 0;
    font-style: italic;
    line-height: 1.65;
    color: var(--muted);
    text-wrap: pretty;
}
.story__quote p { margin: 0; }

/* The pale quote mark, on the right — matching the reference. Absolutely
   positioned against .story__head (not .story), vertically centred on it,
   which is what "not acquiring space" means: it sits on top of the header
   row instead of pushing it down or over to make room. That is a deliberate
   trade-off against a very long name running underneath it, accepted because
   the mark is decorative (low opacity, pointer-events: none) and the
   reference's own name is short enough to clear it too. */
.story__head::after {
    content: "\201D";
    position: absolute;
    top: 50%;
    right: 0;
    transform: translateY(-50%);
    font-size: 46px;
    line-height: 1;
    font-weight: 700;
    color: var(--jade);
    opacity: .32;
    pointer-events: none;
}

/* On the narrowest phones a 54px avatar beside the name leaves too little
   room, so both shrink with the card; the mark shrinks and pulls in with it. */
@media (max-width: 400px) {
    .story__head::after { font-size: 34px; }
    .story__head { gap: 12px; }
    .story__avatar { width: 46px; height: 46px; }
    .story__avatar svg { width: 24px; height: 24px; }
}

/* ── Chain ────────────────────────────────────────── */
/* Understand → Decide → Execute, read left to right. Three items only, so
   unlike the six-across rails this one is sized to sit comfortably rather
   than to squeeze: 320px each plus two 48px gaps is 1056, which is exactly
   the usable width inside the 1120 wrap once its 32px gutters are removed.
   Nothing scrolls on a desktop; on a phone it scrolls sideways and the
   left-to-right reading survives, which is the whole point of the shape. */
.chain {
    margin: var(--sp-5) calc(var(--wrap-pad) * -1) 0;
    padding: 0 var(--wrap-pad);
    display: flex;
    gap: var(--sp-5);
    list-style: none;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scroll-padding-left: var(--wrap-pad);
    overscroll-behavior-x: contain;
    /* Both are needed: scrollbar-width covers Firefox and current Chrome, the
       ::-webkit- rule covers Safari, which does not support the standard one. */
    scrollbar-width: none;
}
.chain::-webkit-scrollbar { display: none; }
.chain:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

.chain__item {
    position: relative;
    flex: 0 0 min(74%, 320px);
    scroll-snap-align: start;
}

/* THE APP'S OWN CHEVRON, not a generic arrow. Icons.jsx exports one back
   glyph — "M7 1L3 5L7 9" in a 10x10 box at 1.5 stroke with round caps — and
   every forward chevron in the app is that same path rotated 180 (see
   .fu-row__chevron). Drawn here already facing right rather than rotated, so
   the shape is identical without a transform. At 14px the 10-unit box scales
   1.4x and the stroke lands at 2.1px, which is what the app renders.

   Background image rather than the character U+2192: Montserrat's preloaded
   subset does not include it. The colour is baked into the data URI because
   currentColor cannot reach inside one; if --jade changes, this hex changes
   with it.

   Centred in BOTH axes. Horizontally: the gap runs from -48 to 0 relative to
   the item, so its middle is -24 and a 14px glyph starts at -31.
   Vertically: 50% of the item, which is the middle of the whole row because
   flex stretches every item to the tallest one. That is what keeps the two
   arrows level with each other even though the three columns hold different
   amounts of text. */
.chain__item:not(:first-child)::before {
    content: "";
    position: absolute;
    left: calc(var(--sp-5) * -.5 - 7px);
    top: 50%;
    transform: translateY(-50%);
    width: 14px;
    height: 14px;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' fill='none' stroke='%2300BB77' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 1L7 5L3 9'/%3E%3C/svg%3E") center / contain no-repeat;
}

.chain__title {
    margin: 0;
    font-size: var(--fs-h3);
    font-weight: 650;
    letter-spacing: -.01em;
    text-wrap: balance;
}
/* THREE LEVELS PER STAGE, and that is the whole reason this version was
   chosen over the numbered "01 / 02 / 03" alternative: a reader can stop at
   any one of them and still have understood something complete.
     .chain__title   the stage, and its place in the order
     .chain__action  what actually happens, in four words
     .chain__text    the detail, for whoever wants it
   The numbered version had no equivalent of the middle line, which is the
   one a first-time visitor gets the most from.

   .chain__action is ink and 650 while the text below it is --muted: the
   weight drop is what separates the two without a rule, an indent or a gap
   big enough to break the stage apart. */
.chain__action {
    margin: 8px 0 0;
    font-size: var(--fs-small);
    font-weight: 650;
    line-height: 1.45;
    letter-spacing: -.005em;
    text-wrap: balance;
}
.chain__text {
    margin: 4px 0 0;
    font-size: var(--fs-small);
    line-height: 1.55;
    color: var(--muted);
    text-wrap: pretty;
}

/* The payoff line. Ink and 650, not grey body text: it is the point of the
   section, and in the secondary colour it would read as a caption hanging off
   the chain above it. --measure keeps it to one readable column rather than
   letting five short sentences run the full 1120. */
.chain__benefits {
    margin: var(--sp-5) 0 0;
    max-width: var(--measure);
    font-size: var(--fs-lead);
    font-weight: 650;
    line-height: 1.5;
    letter-spacing: -.01em;
    text-wrap: pretty;
}

/* .section__title caps at 18ch, which is tuned for the long sentence-style
   titles elsewhere. "How we help you grow" is 20 characters and would break
   after "you" for no reason. */
.section__title--wide { max-width: 24ch; }

/* Fixed 320px items need 1056px, which the wrap only has at its full 1120.
   Between roughly 900 and 1180 that made a laptop scroll a rail it should
   simply fit, so above 900 the three share the row equally and the scrolling
   is left to the phones that actually need it. Not lower than 900: at 768 an
   equal third is 198px, narrower than the same item on a 360px phone. */
@media (min-width: 900px) {
    .chain__item { flex: 1 1 0; min-width: 0; }
}

/* ── Stepper ────────────────────────────────────────────────────────────── */
/* Numbered circles joined by a line, label under each — a checkout-style
   progress bar, laid on its side and made scrollable because five steps with
   readable labels cannot fit across a phone.

   FULL BLEED, by cancelling .wrap's gutter and adding it back as the rail's own
   padding. Two things depend on that: the first circle still lines up with the
   heading above it, and the last step can be scrolled fully clear of the right
   edge. It also means cards run off both edges, which is what tells a visitor
   there is more to see — a rail that stops neatly inside the margin looks
   finished. No 100vw: it includes the scrollbar and has overflowed this page
   before.

   The scrollbar is hidden because the stepper IS the indicator; a native bar
   under it would be a second, uglier one saying the same thing. Keyboard users
   are not left out — the rail carries tabindex="0" so arrow keys scroll it. */
.stepper {
    margin: var(--sp-4) calc(var(--wrap-pad) * -1) 0;
    padding: 0 var(--wrap-pad);
    display: flex;
    /* gap:0 is load-bearing. The connector line is drawn INSIDE each step and
       runs to that step's right edge, so the next circle has to begin exactly
       there or the line would stop short of it. Spacing comes from the step's
       own padding-right instead. */
    gap: 0;
    list-style: none;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scroll-padding-left: var(--wrap-pad);
    overscroll-behavior-x: contain;
    /* Both are needed and neither is redundant: scrollbar-width covers Firefox
       and current Chrome, the ::-webkit- rule covers Safari, which does not
       support the standard property. */
    scrollbar-width: none;
}
.stepper::-webkit-scrollbar { display: none; }
.stepper:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

/* min(72%, 260px): the percentage keeps a sliver of the next step visible on a
   phone, which is the cue that the rail scrolls; the px cap stops the steps
   sprawling on a desktop. Widened from 210px once the step copy grew — at the
   narrower width the descriptions were running to five and six lines each,
   which made a row of tall thin columns rather than a stepper. */
.stp {
    flex: 0 0 min(72%, 260px);
    scroll-snap-align: start;
    padding-right: var(--sp-3);
}

.stp__head { display: flex; align-items: center; }

/* A disc when the step is idle, a pill reading "Step 4" when it is the one in
   view. min-width plus a pill radius rather than a fixed width and 50%: at
   38x38 a 19px radius IS a circle, so the same box does both shapes and there
   is no border-radius to animate between states. Horizontal padding is
   constant for the same reason — collapsed, the numeral alone is narrower than
   min-width, so the padding is slack the circle absorbs. */
.stp__dot {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 38px;
    height: 38px;
    padding-inline: 11px;
    border-radius: 19px;
    /* A filled grey disc, matching the reference. --grey-600 on --grey-100
       measures 4.84:1, which clears the 4.5 this numeral needs — 14px at
       weight 650 is not "large text", that starts at 18.66px bold, so the
       lower 3:1 threshold does not apply. The border adds definition against
       a white page; the reference has none, but the reference was not on
       white. */
    border: 1px solid var(--line);
    background: var(--surface);
    color: var(--muted);
    font-size: var(--fs-small);
    font-weight: 650;
    /* Tabular so 1 and 4 occupy the same width and the circles stay identical. */
    font-variant-numeric: tabular-nums;
    transition: background var(--t-1) var(--ease-std),
                border-color var(--t-1) var(--ease-std),
                color var(--t-1) var(--ease-std);
}

/* The word "Step", which slides open ahead of the numeral.
   grid-template-columns 0fr -> 1fr is the technique, and it is the reason this
   is smooth: it animates to the text's OWN width, so there is no guessed
   max-width to overshoot and snap back from at the end. The inner span carries
   overflow:hidden and min-width:0 — without the min-width a grid item refuses
   to shrink below its content and the column never reaches 0.

   The gap before the numeral is an &nbsp; INSIDE the clipped span, not padding
   on it. Padding would not collapse: with border-box a 0-width box is still at
   least its own padding wide, which would leave a permanent few pixels of
   offset and knock the idle circle off centre. */
.stp__word {
    display: grid;
    grid-template-columns: 0fr;
    /* Out: hold briefly so the text has faded before the squeeze begins. */
    transition: grid-template-columns var(--t-2) var(--ease-out) .1s;
}
.stp__word > span {
    overflow: hidden;
    min-width: 0;
    white-space: nowrap;
    opacity: 0;
    /* Out: leave quickly, and start immediately. */
    transition: opacity .12s var(--ease-std);
}
/* In: the opposite order — the space opens first, the word arrives once there
   is room for it. Asymmetric on purpose; matching both directions makes the
   text look squashed on the way in and stranded on the way out. */
.stp.is-on .stp__word {
    grid-template-columns: 1fr;
    transition: grid-template-columns var(--t-2) var(--ease-out);
}
.stp.is-on .stp__word > span {
    opacity: 1;
    transition: opacity .22s var(--ease-std) .12s;
}
/* Fills the gap between this circle and the next one. Hidden on the last step,
   which has nothing to join to. */
.stp__line {
    flex: 1 1 auto;
    height: 2px;
    background: var(--line);
    transition: background var(--t-1) var(--ease-std);
}
.stp:last-child .stp__line { display: none; }

.stp__label {
    margin: var(--sp-2) 0 0;
    font-size: var(--fs-h3);
    font-weight: 650;
    letter-spacing: -.01em;
    color: var(--muted);
    transition: color var(--t-1) var(--ease-std);
}
.stp__text {
    margin: 4px var(--sp-3) 0 0;
    font-size: var(--fs-small);
    line-height: 1.55;
    color: var(--muted);
    text-wrap: pretty;
}

/* Everything up to the step in view. Set from JS as the rail scrolls, so the
   dark run is always "how far along you have looked" — a scroll position, not a
   claim that anything has been completed. That is also why there is no tick
   mark: nothing here has been done yet. */
.stp.is-on .stp__dot {
    background: var(--grey-900);
    border-color: var(--grey-900);
    color: var(--white);
}
.stp.is-on .stp__line { background: var(--grey-900); }
.stp.is-on .stp__label { color: var(--ink); }

/* No JS: the fill never runs, so every step must already be legible on its own.
   Nothing here depends on the class arriving. */

/* ── Buttons ────────────────────────────────────────────────────────────── */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-1);
    min-height: 52px;
    padding: 14px 24px;
    border: 1px solid transparent;
    border-radius: var(--radius);
    font-family: inherit;
    font-size: 15px;
    font-weight: 600;
    line-height: 1.2;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--t-1) var(--ease-std),
                border-color var(--t-1) var(--ease-std),
                color var(--t-1) var(--ease-std);
}
.btn--solid { background: var(--solid); color: var(--on-solid); }
.btn--solid:hover { background: var(--solid-hover); }
/* --grey-900, not --grey-200: a control boundary needs 3:1 and the lighter
   line manages 1.3:1. */
.btn--ghost { background: transparent; color: var(--ink); border-color: var(--ink); }
.btn--ghost:hover { background: var(--surface); }
.btn--block { width: 100%; }

/* ── Video block ────────────────────────────────────────────────────────── */
/* The poster is the video's own YouTube thumbnail, shown as it is. The
   lettering inside it is artwork rather than page text, so the
   Montserrat-only rule does not reach into it. */
/* NO full-bleed utility is needed or wanted here. .video sits as a direct
   child of .section, which carries no horizontal padding, so it is already
   edge to edge on a phone. Two earlier attempts to "help" it both caused
   horizontal overflow: 100vw (which includes the scrollbar, so ~15px too
   wide on desktop) and then a negative gutter margin (which pushed it past
   an edge it was already touching). The correct amount of CSS was none. */
.video { margin-top: 0; }

/* The video is #video's first element now (section--flush-top drops the
   section's own top padding to match), so the gap that used to sit ABOVE it,
   under a heading, moves to AFTER it instead.
   var(--sp-1), not --sp-4: the client asked for the title to read as
   attached to the video, the way a YouTube title sits close under the
   player rather than floating in its own block of whitespace. */
.video + .wrap { margin-top: var(--sp-1); }

/* The YouTube pattern: a compact, single-line, bold title directly under the
   player — not this page's usual large, balanced, up-to-two-line section
   title. Sized to --fs-h3, the same type this page uses for a sub-heading
   rather than a headline, and forced to one line with an ellipsis rather
   than .section__title's `text-wrap: balance` (which WANTS two lines the
   instant one would look more even). "Start with free Growth Review" is 30
   characters and clears one line at --fs-h3 down to the narrowest phone this
   site supports; the ellipsis is a safety net for a future, longer title,
   not something expected to fire today. max-width and the 18ch cap from
   .section__title are both dropped — nowrap already prevents any wrap, so a
   width cap would only make the ellipsis trigger sooner than it needs to. */
.section__title--video {
    max-width: none;
    font-size: var(--fs-h3);
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ── Video facade ───────────────────────────────────────────────────────── */
/* A poster and a play control that become the real iframe on click. Nothing
   from YouTube is requested until someone actually asks for it, so the page
   costs no third-party bytes, sets no cookies and drops no trackers on a
   visitor who never presses play.
   aspect-ratio sits on BOTH the button and the frame, identical, so the swap
   causes no reflow and the space is reserved before the poster arrives. */
.video__btn {
    position: relative;
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    padding: 0;
    border: 0;
    overflow: hidden;
    background: var(--grey-900);
    cursor: pointer;
}
.video__poster { width: 100%; height: 100%; object-fit: cover; }

/* KEEPS ITS PALETTE VALUES IN BOTH SCHEMES, deliberately. This disc and
   its triangle sit on the video poster, not on the page, so they must not
   follow the page's theme: a dark-mode "white" here would vanish into the
   thumbnail. Same reason .video__btn's backdrop stays --grey-900. */
.video__play {
    position: absolute;
    inset: 0;
    margin: auto;
    width: 68px; height: 68px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: var(--white);
    transition: transform var(--t-1) var(--ease-out);
}
.video__btn:hover .video__play { transform: scale(1.06); }
/* margin-left optically centres a triangle inside a circle: its visual mass
   sits left of its bounding box. */
.video__play svg { width: 26px; height: 26px; margin-left: 4px; fill: var(--grey-900); }

.video__frame { display: block; width: 100%; aspect-ratio: 16 / 9; border: 0; }


/* ── Founder block ──────────────────────────────────────────────────────── */
/* The portrait is an OPTIONAL slot. With no <img> the section still reads as
   a complete first-person statement rather than a layout with a hole in it,
   which is why the grid is defined on the text and the image simply sits
   above it. Cropped from a video frame and in full colour; the grey behind
   it is only the placeholder that holds the box before the image lands. */
/* The top margin lives here, on the mobile base rule, not only in the 640px
   block where it was first written: without it the portrait sat flush
   against the heading with a measured 0px gap on every phone. */
.founder { margin-top: var(--sp-4); }

/* .founder__portrait is gone: the portrait was a 172px thumbnail above the
   message and is now the full-bleed .lede figure before it. */

.founder__quote {
    margin: 0;
    max-width: var(--measure);
}
.founder__quote p { margin: 0 0 var(--sp-3); line-height: 1.65; }
.founder__quote p:last-child { margin-bottom: 0; }

/* DUAL TONE. The ordinary sentences sit at --muted and the <strong> runs
   step up to --ink AND to 650, so the emphasis carries on two axes rather
   than one. That is what makes the bold lines skimmable as the argument on
   their own, with the rest reading as the working-out underneath.

   Italic is set on the paragraph, so the <strong> inside inherits it:
   Montserrat is self-hosted as a variable ITALIC face across 100-900
   (see the @font-face block at the top of this file), which means bold
   italic here is a real drawn weight, not a browser-synthesised slant. */
.founder__message {
    font-style: italic;
    color: var(--muted);
    text-wrap: pretty;
}
.founder__message strong {
    font-weight: 650;
    color: var(--ink);
}

/* Scoped to beat `.founder__quote p` (0,1,1), which this rule as a bare
   class (0,1,0) lost to — its margin-top was silently dead, and the gap
   above the signature was actually coming from the margin-BOTTOM of
   whatever paragraph happened to precede it. That worked until something
   other than a <p> sat above it, and then the name collided with it.
   text-align: right at the client's instruction — the signature reads as a
   closing attribution at the column's right edge now, not a continuation
   of the paragraph's left edge. */
.founder__quote .founder__sign { margin: var(--sp-3) 0 0; text-align: right; }
.founder__name { display: block; font-weight: 650; }
.founder__role { display: block; font-size: var(--fs-small); color: var(--muted); }


/* ── FAQ ────────────────────────────────────────────────────────────────── */
/* Hairline rows, the page's own idiom, rather than twelve bordered cards.
   Native <details>, so there is no JS here at all: the open/close state, the
   keyboard handling and the screen reader semantics are the browser's. */
.faq { margin: var(--sp-4) 0 0; border-top: 1px solid var(--line); }
.faq__item { border-bottom: 1px solid var(--line); }

.faq__q {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    /* 18px block padding puts the row at 55px tall, past the 44px minimum a
       tap target needs, without a min-height that would look wrong once a
       question wraps to two lines on a phone. */
    padding-block: 18px;
    font-size: var(--fs-lead);
    font-weight: 650;
    line-height: 1.4;
    letter-spacing: -.01em;
    cursor: pointer;
    /* Kills the default disclosure triangle. list-style covers Firefox and
       current Chrome; the ::-webkit- rule below is what Safari still needs. */
    list-style: none;
    text-wrap: pretty;
}
.faq__q::-webkit-details-marker { display: none; }
.faq__q:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }

/* The chevron is drawn from two borders rather than an SVG or a glyph, and
   that is the point: border-color inherits currentColor, so it follows the
   theme with no second asset, no data URI (which cannot take currentColor)
   and no twelve copies of the same inline SVG. */
.faq__q::after {
    content: "";
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    margin-right: 4px;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    color: var(--muted);
    transform: translateY(-2px) rotate(45deg);
    transition: transform var(--t-1) var(--ease-std);
}
.faq__item[open] .faq__q::after { transform: translateY(2px) rotate(-135deg); }

/* Only the chevron rotates. The panel itself does not animate open: a height
   transition on <details> needs interpolate-size or ::details-content, both
   too new to rely on, and the usual workaround (a wrapper with max-height)
   either clips long answers or eases towards a guessed number. An instant
   open that is always correct beats an animation that sometimes truncates. */
.faq__a {
    padding-bottom: var(--sp-3);
    max-width: var(--measure);
}
.faq__a p { margin: 0; line-height: 1.65; color: var(--muted); }
.faq__a p + p { margin-top: var(--sp-2); }
/* The direct answer, before the explanation. --ink against the --muted
   paragraph under it, so someone who reads only the bold line still gets a
   complete answer. */
.faq__a strong { font-weight: 650; color: var(--ink); }

/* ── Brand mark ─────────────────────────────────────────────────────────── */
/* The header's logo, scaled up to fill the wrap: full width of the content
   column, same left/right gutter as every other section (.wrap's own
   --wrap-pad), not a true edge-to-edge bleed to the viewport. "Touching left
   to right" reads correctly at the column's edges without breaking the
   margin every other element on the page respects.
   height: auto keeps the SVG's own aspect ratio; width/height attributes on
   the <img> are the intrinsic size (prevents a layout jump before it loads),
   the CSS width is what actually scales it up. */
.brand-mark { padding-block: var(--sp-5); border-top: 1px solid var(--line); }
/* display: block on the <picture> too, not just the <img> inside it: a
   <picture> is inline by default, which would leave a line-height gap under
   the mark that no padding value explains. */
.brand-mark picture { display: block; }
.brand-mark img { display: block; width: 100%; height: auto; }

/* ── Footer ─────────────────────────────────────────────────────────────── */
.site-footer {
    padding-block: var(--sp-5);
    border-top: 1px solid var(--line);
}
.site-footer__brand { font-weight: 700; letter-spacing: -.01em; }
.site-footer__tag   { margin: 2px 0 var(--sp-3); font-size: var(--fs-small); color: var(--muted); }
.site-footer__links { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); margin: 0; padding: 0; list-style: none; }
/* 11px, not 10: at 10 the link box measures 42px, which clears the 24px WCAG
   AA minimum but misses the 44px target this project holds itself to
   everywhere else. Two pixels, and the whole page passes one rule instead of
   two. */
.site-footer__links a { display: inline-block; padding-block: 11px; font-size: var(--fs-small); color: var(--muted); text-decoration: none; }
.site-footer__links a:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.site-footer__legal { margin: var(--sp-3) 0 0; font-size: 13px; color: var(--muted); }

/* ── Fixed call to action ───────────────────────────────────────────────── */
/* FIXED, not sticky. body carries overflow-x: hidden, which makes body the
   scrollport, and a sticky bar would then stick to a box that scrolls with
   the page — it would simply never appear to work.
   Down while the hero's own CTA button is on screen, up otherwise — one
   condition (see site.js), so the bar changes state once per pass down the
   page instead of flapping near a second boundary. visibility (not display)
   so the transition has something to animate and the bar stays out of the
   tab order while hidden. */
.cta-bar {
    position: fixed;
    inset: auto 0 0;
    z-index: 60;
    padding: var(--sp-2) 20px calc(var(--sp-2) + env(safe-area-inset-bottom));
    /* The same glass as the header: lit from above, so the denser edge of the
       tint and the bevel are both at the TOP. Two panes of one material, not
       two different effects — which includes casting nothing onto the page.
       This one had a shadow thrown upward and it is gone for the same reason
       the header's is: over light content it read as a haze bleeding out of
       the bar rather than as a pane floating over it. */
    background-image: linear-gradient(to bottom, var(--glass-tint-a), var(--glass-tint-b));
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    border-top: 1px solid var(--glass-line);
    box-shadow: inset 0 1px 0 var(--glass-bevel);
    transform: translateY(100%);
    visibility: hidden;
    /* 1.5x the shared --t-2, not a fourth duration invented for its own sake:
       this bar travels its own full height, further than anything else on
       the page transitions, and at the standard .3s that distance read as a
       flick rather than a glide. Scaling the existing token keeps it tied to
       the same scale while giving the one full-width reveal on the page the
       extra time it needs to look deliberate.

       visibility is switched at 0s with a DELAY rather than transitioned over
       the duration. Transitioning it relies on the special-case interpolation
       browsers apply to visibility, which is easy to get subtly wrong in
       either direction; stepping it explicitly is unambiguous. Going down,
       the delay holds the bar visible for the whole slide and only then
       removes it from the tab order — without it the bar would blink out
       partway down. Going up (below) the delay is 0, so it becomes visible on
       the first frame and the whole slide is seen. */
    transition: transform calc(var(--t-2) * 1.5) var(--ease-out),
                visibility 0s linear calc(var(--t-2) * 1.5);
}
.cta-bar.is-visible {
    transform: none;
    visibility: visible;
    transition: transform calc(var(--t-2) * 1.5) var(--ease-out),
                visibility 0s linear 0s;
}
.cta-bar__inner { max-width: var(--wrap); margin-inline: auto; }

/* ── 640px and up ───────────────────────────────────────────────────────── */
@media (min-width: 640px) {
    :root { --wrap-pad: 32px; }
    .site-header__bar { min-height: 80px; }
    .site-header__logo img { height: 24px; }

    /* Reserve the header's height inside the hero. The header is fixed and
       transparent over the hero, so it costs the hero no layout — which was
       fine while the hero held one short heading and an upward lift, but the
       hero now holds four stacked things. Centred inside the full 92svh, the
       top of the heading landed at y=23 on a 1280x700 window, i.e. underneath
       an 80px header. Padding here means the centring happens in the space
       BELOW the header rather than behind it. box-sizing is border-box
       globally, so this comes out of the 92svh rather than adding to it, and
       min-height still lets the section grow if the content needs more.
       Desktop only: the phone branch already reserves its own, larger gap on
       .hero .wrap, and adding this on top would double it. */
    .hero { padding-top: 80px; }

    :root { --section-y: var(--sp-6); }

    /* Edge to edge is right on a phone and excessive on a wide screen, so
       above 640 the video lines up with the text instead. */
    .video { max-width: var(--wrap); margin-inline: auto; padding-inline: var(--wrap-pad); }

    /* The portrait used to sit in a 200px column beside the words. It is the
       full-bleed photo above them now, so the message is the only thing left
       here and a two-column grid would leave an empty column. */

    .btn--block { width: auto; }
}

/* ═══ Reviewed-business profile pages (/reviewed/…) ═══════════════════════
   Generated by scripts/build-profiles.mjs. These are the only pages on the
   site that are not the homepage, and they are deliberately plain: a profile
   is a record, not a pitch, and a page that tries to sell on someone else's
   name reads as advertising rather than a citation. Tokens and type come from
   the same :root as everything above, so they inherit dark mode for free. */
.rvw {
    max-width: 640px;
    margin: 0 auto;
    padding: var(--sp-6) var(--wrap-pad) var(--sp-6);
}

.rvw__eyebrow {
    margin: 0 0 28px;
    font-size: 13px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--muted);
}
.rvw__eyebrow a { color: inherit; text-decoration: none; }
.rvw__eyebrow a:hover { color: var(--ink); }

/* 5:4, the shape it was cropped to. aspect-ratio plus a width cap means the
   slot is the right size before the file arrives, so nothing below it jumps. */
.rvw__photo {
    display: block;
    width: min(340px, 100%);
    /* height:auto is load-bearing, not tidiness. The width/height ATTRIBUTES
       are there so the browser reserves the right box before the file lands;
       without height:auto the 1000 in that attribute wins and aspect-ratio is
       ignored, which renders the portrait as a 210x1000 sliver. */
    height: auto;
    aspect-ratio: 5 / 4;
    object-fit: cover;
    border-radius: var(--radius);
    background: var(--surface);
    margin: 0 0 var(--sp-3);
}

.rvw__name {
    margin: 0 0 8px;
    font-size: clamp(28px, 6vw, 40px);
    line-height: 1.15;
    letter-spacing: -0.02em;
    font-weight: 700;
    color: var(--ink);
    /* A business name is arbitrary text, and some are long single words. */
    overflow-wrap: anywhere;
}

.rvw__meta { margin: 0 0 36px; font-size: 15px; color: var(--muted); }

/* Label above value, not beside it: the values here (a service description, a
   domain) are long enough that a two-column row would wrap into a mess on a
   phone, which is where these pages are read. */
.rvw__facts { margin: 0 0 36px; padding: 0; }
.rvw__fact { margin: 0 0 18px; }
.rvw__fact dt {
    margin: 0 0 3px;
    font-size: 12px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--muted);
}
.rvw__fact dd { margin: 0; font-size: 16px; line-height: 1.5; color: var(--ink); overflow-wrap: anywhere; }
.rvw__fact dd a { color: var(--ink); text-underline-offset: 3px; }

/* Their words, set apart from ours. The quote comes first on the page and
   what_we_fixed follows it, because a stranger believes the business before
   they believe us. */
.rvw__quote {
    margin: 0 0 36px;
    padding: 0 0 0 var(--sp-3);
    border-left: 2px solid var(--line);
}
.rvw__quote p {
    margin: 0 0 10px;
    font-size: 17px;
    line-height: 1.6;
    color: var(--ink);
}
.rvw__cite {
    font-size: 14px;
    font-style: normal;
    color: var(--muted);
}

.rvw__block { margin: 0 0 36px; }
.rvw__block h2 {
    margin: 0 0 10px;
    font-size: 17px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--ink);
}
.rvw__block p { margin: 0 0 12px; font-size: 16px; line-height: 1.65; color: var(--muted); }

.rvw__cta { padding-top: 28px; border-top: 1px solid var(--line); }
.rvw__btn {
    display: inline-block;
    margin-top: 4px;
    padding: 13px 22px;
    border-radius: 999px;
    background: var(--solid);
    color: var(--on-solid);
    font-size: 15px;
    font-weight: 600;
    text-decoration: none;
}

/* Says outright that this is not paid placement. The page names a real
   business, so the basis for naming them belongs on it. */
.rvw__note {
    margin: 44px 0 0;
    font-size: 12.5px;
    line-height: 1.6;
    color: var(--muted);
}

.rvwl { list-style: none; margin: 0; padding: 0; }
.rvwl__row { border-bottom: 1px solid var(--line); }
.rvwl__row a {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 14px 0;
    text-decoration: none;
}
.rvwl__photo {
    flex: 0 0 auto;
    width: 60px;
    height: auto;      /* see .rvw__photo */
    aspect-ratio: 5 / 4;
    object-fit: cover;
    border-radius: 8px;
    background: var(--surface);
}
.rvwl__text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.rvwl__name { font-size: 17px; font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }
.rvwl__meta { font-size: 14px; color: var(--muted); }


/* ── The expanded review card (/reviewed/<slug>/) ────────────────────────
   The homepage card, opened up: the same 5:4 photo at full width, then the
   name, designation and the WHOLE quote (the card clamps it to three lines). */
.rvx { margin: 0 0 var(--sp-4); }
.rvx__photo {
    display: block;
    width: 100%;
    height: auto;          /* the width/height attributes would otherwise win */
    aspect-ratio: 5 / 4;
    object-fit: cover;
    border-radius: 18px;
    background: var(--surface);
    margin: 0 0 var(--sp-3);
}
.rvx__name {
    margin: 0 0 4px;
    font-size: clamp(26px, 6vw, 34px);
    line-height: 1.2;
    letter-spacing: -0.02em;
    font-weight: 700;
    color: var(--ink);
    overflow-wrap: anywhere;
}
.rvx__role { margin: 0 0 var(--sp-3); font-size: 15px; color: var(--muted); }
.rvx__quote { margin: 0; }
.rvx__quote p {
    margin: 0;
    font-size: 18px;
    line-height: 1.65;
    font-style: italic;
    color: var(--ink);
    white-space: pre-line;     /* keeps the paragraph breaks they typed */
}

/* Homepage cards are links to that page. The anchor wraps the figure, so
   the whole card is the target, and it inherits the card's look untouched. */
.story__link {
    display: block;
    height: 100%;
    color: inherit;
    text-decoration: none;
    border-radius: 18px;
}
.story__link .story { transition: transform .2s var(--ease-out); }
.story__link:hover .story { transform: translateY(-2px); }
.story__link:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }


/* ── Review page header: the mobile app's PageHeader, ported ─────────────
   Same measurements as apps/business-manager/mobile PageHeader: a 48x48
   back button holding an 18x24 chevron, the title beside it at 18px bold,
   8px side padding, and a 1px hairline under the bar. Sticky so the way
   back is always one tap away on a long quote. Colours come from the site
   tokens, so it follows light and dark like everything else. */
.rvh {
    position: sticky;
    top: 0;
    z-index: 10;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: env(safe-area-inset-top) 8px 4px;
    padding-top: calc(env(safe-area-inset-top) + 8px);
    background: var(--bg);
    border-bottom: 1px solid var(--line);
}
.rvh__back {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 48px;
    height: 48px;
    border-radius: 50%;
    color: var(--ink);
    text-decoration: none;
    -webkit-tap-highlight-color: transparent;
}
.rvh__back:active { background: var(--surface); }
.rvh__back:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
.rvh__title {
    flex: 1;
    min-width: 0;
    font-size: 18px;
    font-weight: 700;
    color: var(--ink);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* The header now carries the way back, so the page no longer needs the
   big top gap the old eyebrow link sat in. */
.rvh + .rvw { padding-top: var(--sp-3); }


/* Growth Story card text, a size down so more of the quote fits the fixed
   5:4 card before it is clamped. Name 18 -> 16, role 14 -> 13, quote 16 -> 14. */
.story .story__name { font-size: 16px; }
.story .story__role { font-size: 13px; }
.story .story__quote,
.story .story__quote p { font-size: 14px; line-height: 1.55; }


/* Heading above the quote on the review page: says the quote is the
   business talking about us, not a description of the business above it. */
.rvx__heading {
    margin: 0 0 10px;
    font-size: 17px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--ink);
}


/* ── Card layout: quote above, attribution bottom right ──────────────────
   The quote reads left to right as before; the name and designation sit
   under it, pinned to the bottom-right corner and right-aligned, so they
   grow leftwards. Card only: the review page keeps its own layout. The
   decorative quote glyph is dropped, since the text now carries its own
   quotation marks and the glyph would collide with right-aligned text. */
.story .story__quote { margin: 0; }
.story .story__head {
    margin-top: auto;              /* pushes the attribution to the bottom */
    padding-top: var(--sp-1);
    flex-direction: row-reverse;   /* avatar (plain cards) ends up rightmost */
    justify-content: flex-start;
    text-align: right;
}
.story .story__id { display: flex; flex-direction: column; align-items: flex-end; }
.story .story__head::after { display: none; }
/* On a photo card everything sits at the bottom over the fade; there the
   quote is not pushed away from the name, it sits just above it. */
.story--photo .story__head { margin-top: 0; }


/* ── Google-style rating stars ────────────────────────────────────────────
   Google's colours: #FBBC04 for filled stars; #DADCE0 for empty ones on a
   light page and #5F6368 on a dark one. The yellow layer sits on top of the
   grey one and is clipped to the average, so fractions show as a part-filled
   star. The stars scale with the number beside them (em-based). */
:root { --gstar-empty: #DADCE0; }
@media (prefers-color-scheme: dark) { :root { --gstar-empty: #5F6368; } }

.gstars {
    position: relative;
    display: inline-block;
    vertical-align: middle;
    line-height: 0;
    white-space: nowrap;
}
.gstars svg { width: 1em; height: 1em; display: inline-block; }
.gstars__base svg { fill: var(--gstar-empty); }
.gstars__fill {
    position: absolute;
    inset: 0 auto 0 0;
    overflow: hidden;
}
.gstars__fill svg { fill: #FBBC04; }

/* In the homepage figures: the number, then the stars at a size that reads
   as belonging to it rather than competing with it. */
#statRating { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
#statRating .gstars { font-size: 0.5em; }


/* ── The fade sits only behind the text ──────────────────────────────────
   .story__over wraps the quote and the attribution. On a plain card it is
   just the column the two sit in. On a photo card it is the fade itself:
   its top is the top of the quote, it runs out to the card's left, right
   and bottom edges (negative margins cancel the card's padding), and
   everything above it shows the photo untouched. The ramp over the first
   few pixels keeps its top edge from reading as a hard line. */
.story__over { display: flex; flex-direction: column; flex: 1 1 auto; }
.story--photo .story__over {
    position: relative;
    z-index: 1;
    flex: 0 0 auto;
    margin: 0 calc(var(--sp-2) * -1) calc(var(--sp-2) * -1);
    padding: 18px var(--sp-2) var(--sp-2);   /* the blend runs in these 18px */
    /* 70% at the card's bottom edge, fully clear at the top of this layer,
       eased over its top 46px: it starts in the 18px above the quote and
       finishes inside the quote's first line, so the text sits on a solid
       enough fade to read and the ramp is long enough to leave no edge. */
    background: linear-gradient(to top,
        rgba(var(--story-fade), .7) 0%,
        rgba(var(--story-fade), .66) calc(100% - 46px),
        rgba(var(--story-fade), .59) calc(100% - 32px),
        rgba(var(--story-fade), .46) calc(100% - 22px),
        rgba(var(--story-fade), .29) calc(100% - 14px),
        rgba(var(--story-fade), .14) calc(100% - 7px),
        rgba(var(--story-fade), .04) calc(100% - 3px),
        rgba(var(--story-fade), 0) 100%);
}



/* The gap between the quote and the name, tightened. --sp-2 (14px) read as
   loose next to the 3px between the name and its role below — 5x the gap
   for one line break less. --sp-1 keeps the two blocks visually separate
   without the quote reading as detached from who said it. */
.story--photo .story__head { padding-top: var(--sp-1); }
