/* CCD LANDING PAGE CSS - assets/home.css
   Loaded after assets/tokens.css and assets/site.css, and after the small
   inline critical block that stays in index.html's own <head>. Order is
   load-bearing: tokens, then site, then the inline critical block, then
   this file - see index.html's <head> for why the <link> to this file
   sits below the inline <style>, not above it.

   WHAT THIS FILE IS
   Everything the landing page's original single inline <style> block held
   EXCEPT the hero. Split out in Wave 6 (P6.1) because the inline block was
   119,657 bytes - 58 percent of index.html - sitting where a browser can
   never cache it: every visit re-downloaded the whole stylesheet with the
   markup, even when nothing in it had changed. This file gets a content
   hash and a year of immutable caching from tools/build.mjs and _headers;
   a repeat visitor now re-fetches none of it.

   WHAT STAYS INLINE INSTEAD
   The scroll-reveal base rule and the entire hero section, including the
   Gmail compose mock, at index.html:88 onward - roughly 12.8 KB. That is
   what a browser needs to paint the first viewport correctly before this
   file's request round trip completes. It is a genuine first-paint
   boundary, not a rough guess: everything below "Why-us bento" is content
   the visitor cannot see without scrolling, so it can afford the extra
   request.

   THIS IS A MOVE, NOT A REWRITE
   Every declaration below is byte-for-byte identical to how it read inside
   index.html's inline block, in the same relative order. Nothing was
   tokenized, tidied, deduped or "fixed" while it moved - see
   HANDOVER-opinions.md's "Findings that must NOT be re-derived" for why
   several things below look inconsistent on purpose: the bare "ease"
   keyword is not the same value as var(--ease-out); dozens of animation
   durations are choreography arithmetic the page's own comments spell out
   in place, not a scale; and the eight color-mix(...0%, transparent) calls
   fade through transparent black deliberately, never collapse them to a
   bare "transparent" keyword.

   The trailing responsive block at the end of this file re-declares some
   of the same selectors the hero uses (.hero, .hero .container, .bento,
   .fit-grid, .gmail-body, .gmail-sig, .gmail-tools) as narrower-viewport
   overrides. That is why this file's <link> must stay AFTER the inline
   critical block in index.html's <head>: those overrides need to win the
   cascade tie-break over the inline base rules the same way they did when
   both lived in one block, and moving this file earlier would invert that
   without changing a single selector. */


  /* Why-us bento */
  .bento {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-auto-rows: 1fr;
    /* the cards read their own column width off this container to derive the
       proportion floor — see .bcard's min-height. The gap goes through the
       custom property so the floor's arithmetic can never drift from the real
       gutter; --bento-cols must be kept in step with grid-template-columns in
       every breakpoint below. */
    container-type: inline-size;
    --bento-cols: 3;
    --bento-gap: var(--gap-card);
    gap: var(--bento-gap);
  }
  .bcard {
    position: relative;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    /* Vinyl's exact card proportion, 419/530, expressed as a FLOOR rather than
       as aspect-ratio. A card keeps the proportion when its content fits and
       simply grows when it doesn't; grid-auto-rows: 1fr then levels the whole
       row up to the tallest, so the composition stays flush.

       Why not aspect-ratio, which is what this used to be: the ratio is a CAP,
       and overflow: hidden slices whatever is taller. Between roughly 641 and
       1200 the columns get narrow enough that the ratio lands shorter than the
       tallest card's content and headings were cut mid-glyph. Two dead ends,
       both measured, do not repeat them:
         - min-height: fit-content is INERT against aspect-ratio. A ratio plus
           a definite width REPLACES the box's intrinsic block size, so
           fit-content, min-content and max-content all resolve back to the
           ratio height. Same reason a grid item's automatic minimum size can't
           rescue it: that is min(transferred size, content size), and the
           transferred size is the ratio height.
         - dropping aspect-ratio inside one arbitrary band (the tried-and-
           rejected 641-1100 block) leaves the top of the band still clipping
           and shrinks cards whose content is SHORTER than the ratio, which
           rewrites a composition that was never broken.
       The floor has to be the column width times the ratio, and the only way
       to name the column width in CSS is to ask the grid: 100cqw is .bento's
       content box, minus the gaps, over the column count. */
    min-height: calc(
      (100cqw - (var(--bento-cols) - 1) * var(--bento-gap)) / var(--bento-cols)
      * 530 / 419
    );
    background: var(--bg-card);
    border: 0.5px solid var(--hairline);
    border-radius: var(--radius);
    padding: 30px 30px 28px;
    box-shadow: var(--shadow);
    transition: transform 0.4s var(--ease-out), box-shadow 0.4s ease;
  }
  .bcard:hover {
    transform: translateY(-4px);
    box-shadow: 0 2px 4px color-mix(in srgb, var(--accent-ink) 5%, transparent), 0 20px 40px -20px color-mix(in srgb, var(--accent-ink) 18%, transparent);
  }
  .bcard h3 {
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
    line-height: 1.5;
    text-align: center;
    max-width: 26ch;
    margin: 0 auto;
  }
  .bcard--dark { background: var(--grad-ink); border-color: transparent; }
  .bcard--accent { background: var(--grad-accent-deep); border-color: transparent; }
  .bcard--dark h3, .bcard--accent h3 { color: var(--on-accent); }
  .bcard--soft { background: var(--accent-soft); }
  .bviz { margin-top: auto; padding-top: 26px; }
  /* tiles whose visual is shorter than the card: heading + visual ride the
     middle together instead of leaving a dead gap under the heading */
  .bcard--mid { justify-content: center; }
  .bcard--mid .bviz { margin-top: 0; padding-top: 30px; }


  /* viz 1 — hub logo, three connector lines, deck-shuffle app tiles (dark card) */
  .viz-tools { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; }
  .hub-tile {
    width: 105px; height: 105px;
    border-radius: var(--r-circle);
    background: var(--bg-card);
    display: grid;
    place-items: center;
    position: relative;
    z-index: 1;
    box-shadow: 0 10px 26px -10px color-mix(in srgb, var(--accent-ink) 45%, transparent);
  }
  .hub-tile img { width: 56px; height: 56px; display: block; }
  .hub-branch { position: relative; width: 308px; height: 96px; }
  .branch-glow {
    position: absolute;
    left: calc(50% - 12px); top: 0;
    width: 24px; height: 30px;
    background: linear-gradient(180deg, color-mix(in srgb, var(--accent-soft) 25%, transparent), color-mix(in srgb, var(--accent-soft) 0%, transparent));
  }
  .branch-lines { display: block; width: 308px; height: 96px; }
  .branch-lines path { fill: none; stroke: color-mix(in srgb, var(--on-accent) 18%, transparent); stroke-width: 2; stroke-linecap: round; }
  /* travelling glow pulses along the lines, one shot when the section reveals */
  .pulse { position: absolute; opacity: 0; pointer-events: none; }
  .pulse-mid, .pulse-dl, .pulse-dr {
    width: 2px; height: 21px;
    background: linear-gradient(180deg, color-mix(in srgb, var(--accent-bright) 0%, transparent) 0%, var(--accent-bright) 50%, color-mix(in srgb, var(--accent-bright) 0%, transparent) 100%);
  }
  .pulse-hl, .pulse-hr {
    width: 21px; height: 2px;
    background: linear-gradient(90deg, color-mix(in srgb, var(--accent-bright) 0%, transparent) 0%, var(--accent-bright) 50%, color-mix(in srgb, var(--accent-bright) 0%, transparent) 100%);
  }
  .pulse-mid { left: 153px; top: 0; }
  .pulse-hl { left: 133px; top: 29px; }
  .pulse-hr { left: 154px; top: 29px; }
  .pulse-dl { left: 43px; top: 33px; }
  .pulse-dr { left: 263px; top: 33px; }
  /* constant subtle stream: a pulse is always mid-travel, faded in and out at the ends */
  @keyframes pl-mid { 0% { transform: translateY(0); opacity: 0; } 12% { opacity: 0.55; } 88% { opacity: 0.5; } 100% { transform: translateY(75px); opacity: 0; } }
  @keyframes pl-hl { 0% { transform: translateX(0); opacity: 0; } 12% { opacity: 0.55; } 88% { opacity: 0.5; } 100% { transform: translateX(-89px); opacity: 0; } }
  @keyframes pl-hr { 0% { transform: translateX(0); opacity: 0; } 12% { opacity: 0.55; } 88% { opacity: 0.5; } 100% { transform: translateX(89px); opacity: 0; } }
  @keyframes pl-drop { 0% { transform: translateY(0); opacity: 0; } 12% { opacity: 0.55; } 88% { opacity: 0.5; } 100% { transform: translateY(42px); opacity: 0; } }
  .js .reveal.in .pulse-mid { animation: pl-mid 2.4s linear 0.5s infinite; }
  .js .reveal.in .pulse-hl { animation: pl-hl 2.4s linear 0.5s infinite; }
  .js .reveal.in .pulse-hr { animation: pl-hr 2.4s linear 0.5s infinite; }
  .js .reveal.in .pulse-dl, .js .reveal.in .pulse-dr { animation: pl-drop 2.4s linear 0.5s infinite; }
  .tool-stacks { display: flex; gap: 22px; }
  .tstack { position: relative; width: 88px; height: 102px; }
  .tcard {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: 88px;
    background: var(--bg-card);
    border-radius: var(--mr-xl);
    display: grid;
    place-items: center;
    transition: transform 0.9s ease, opacity 0.9s ease;
  }
  .tcard img { width: 44px; height: 44px; object-fit: contain; display: block; }
  /* deck states — JS rotates these classes; transitions keep every move eased */
  .tcard.is-top { transform: none; opacity: 1; z-index: 3; }
  .tcard.is-mid { transform: translateY(-8px) scale(0.94); opacity: 0.6; z-index: 2; }
  .tcard.is-back { transform: translateY(-14px) scale(0.88); opacity: 0.35; z-index: 1; }
  .tcard.is-out { transform: translateY(-14px) scale(0.88); opacity: 0; z-index: 4; }
  .tcard.no-trans { transition: none; }

  /* viz 2 — iMessage thread */
  .viz-msg { display: flex; flex-direction: column; gap: 5px; font-family: var(--sf); padding-top: 2px; }
  .msg-day { text-align: center; font-size: var(--fs-2xs); color: var(--muted); margin-bottom: 2px; }
  .msg-row--pad.msg-row--in { padding-left: 34px; }
  .msg-row--pad.msg-row--out { padding-right: 34px; }
  .msg-row { display: flex; align-items: flex-end; gap: 8px; }
  .msg-row--out { justify-content: flex-end; }
  .msg-b {
    max-width: 90%;
    padding: 7px 12px;
    border-radius: var(--mr-lg);
    font-size: var(--fs-sm);
    line-height: 1.4;
  }
  .msg-b--in { background: var(--imsg-bubble-in); color: var(--mock-ink); border-bottom-left-radius: 5px; }
  .msg-b--out { background: var(--imsg-bubble-out); color: var(--mock-white); border-bottom-right-radius: 5px; }
  /* iMessage "loved" tapback on the thank-you bubble: blue balloon, glossy
     pink heart, tail dots trailing down-right toward the message */
  @keyframes react-push { from { margin-top: 0; } to { margin-top: 26px; } }
  .js .reveal.in .msg-row--react { animation: react-push 0.45s var(--ease-out) 12.6s both; }
  .msg-b { position: relative; z-index: 0; }
  .msg-react {
    position: absolute;
    top: -28px; left: -16px;
    width: 26px; height: 26px;
    border-radius: var(--r-circle);
    background: var(--imsg-bubble-in);
    display: grid;
    place-items: center;
    z-index: -1;
  }
  .msg-react::before, .msg-react::after {
    content: '';
    position: absolute;
    border-radius: var(--r-circle);
    background: var(--imsg-bubble-in);
  }
  .msg-react::before { width: 8px; height: 8px; right: -2px; bottom: -1px; }
  .msg-react::after { width: 4px; height: 4px; right: -2px; bottom: -4px; }
  .msg-react svg { display: block; }
  @keyframes react-pop {
    0% { opacity: 0; transform: scale(0); }
    60% { opacity: 1; transform: scale(1.15); }
    100% { opacity: 1; transform: scale(1); }
  }
  .js .reveal .msg-react { opacity: 0; }
  .js .reveal.in .msg-react { animation: react-pop 0.45s var(--ease-out) 12.6s both; }
  /* iPhone receipts: Delivered under each sent message, gone when the next
     one sends; the last flips Delivered -> Read and stays */
  .msg-status {
    position: relative;
    text-align: right;
    font-size: var(--fs-xs);
    color: var(--muted);
    padding-right: 38px;
    margin-top: -2px;
    /* The 14px slot is RESERVED, not animated. st-flash used to run
       height 0 -> 14px -> 0, which shoved every bubble below it up and down
       twice per receipt; the whole thread jittered mid-playback. Height is now
       constant and only opacity moves. It costs ~21px of permanent card height
       (the rows that used to collapse back to zero). That used to be a problem:
       while .bcard's 419/530 proportion was an aspect-ratio it capped the card,
       and between 641 and 1200 the card was already too short for this thread —
       34px over at 1100, 50px at 1024, 113px at 901 — with the reserved slots
       deepening the clip. The proportion is now a floor (see .bcard), so the
       card grows to fit the thread and the reserved slots cost nothing but
       height. Keep them constant anyway: the jitter is the reason. */
    height: 14px;
    opacity: 0;
    overflow: hidden;
  }
  .msg-status em { font-style: normal; position: absolute; right: 38px; top: 0; opacity: 0; }
  @keyframes st-flash { 0% { opacity: 0; } 8% { opacity: 1; } 92% { opacity: 1; } 100% { opacity: 0; } }
  @keyframes st-appear { to { opacity: 1; } }
  @keyframes st-del { 0% { opacity: 0; } 15% { opacity: 1; } 85% { opacity: 1; } 100% { opacity: 0; } }
  @keyframes st-read { from { opacity: 0; } to { opacity: 1; } }
  /* order per exchange: sent -> Delivered -> Read -> typing -> reply
     (you read a message before you start typing the answer) */
  .js .reveal.in .msg-status--1 { animation: st-flash 4.2s ease 1s both; }
  .js .reveal.in .msg-status--2 { animation: st-flash 4.4s ease 5.6s both; }
  .js .reveal.in .msg-status--3 { animation: st-appear 0.4s ease 10.8s forwards; }
  /* Delivered and Read share one absolutely-positioned slot, so their fades
     must not overlap or the two strings render as one double-struck word.
     Each st-del duration is sized to land its 85%->100% fade-out exactly on
     the matching st-read delay: 1 + 0.8 = 1.8, 5.6 + 0.9 = 6.5, 10.8 + 1.2 =
     12. Start delays are untouched — the 13s choreography is unchanged. */
  .js .reveal.in .msg-status--1 .msg-st-delivered { animation: st-del 0.8s ease 1s both; }
  .js .reveal.in .msg-status--1 .msg-st-read { animation: st-read 0.4s ease 1.8s both; }
  .js .reveal.in .msg-status--2 .msg-st-delivered { animation: st-del 0.9s ease 5.6s both; }
  .js .reveal.in .msg-status--2 .msg-st-read { animation: st-read 0.4s ease 6.5s both; }
  .js .reveal.in .msg-status--3 .msg-st-delivered { animation: st-del 1.2s ease 10.8s both; }
  .js .reveal.in .msg-status--3 .msg-st-read { animation: st-read 0.4s ease 12s both; }
  /* typing indicator: CCD typing on the left, where the reply will land */
  .msg-row--in, .msg-row--out { position: relative; }
  .msg-typing {
    position: absolute;
    left: 34px; bottom: 0;
    display: inline-flex;
    gap: 4px;
    align-items: center;
    background: var(--imsg-bubble-in);
    border-radius: var(--mr-lg);
    border-bottom-left-radius: 5px;
    padding: 10px 12px;
    opacity: 0;
    pointer-events: none;
  }
  .msg-typing i {
    width: 6px; height: 6px;
    border-radius: var(--r-circle);
    background: var(--imsg-grey);
    animation: typing-dot 1.2s ease-in-out infinite;
  }
  .msg-typing i:nth-child(2) { animation-delay: 0.15s; }
  .msg-typing i:nth-child(3) { animation-delay: 0.3s; }
  @keyframes typing-dot {
    0%, 60%, 100% { transform: none; opacity: 0.55; }
    30% { transform: translateY(-3px); opacity: 1; }
  }
  @keyframes typing-pop {
    0% { opacity: 0; transform: translateY(4px); }
    10% { opacity: 1; transform: none; }
    90% { opacity: 1; }
    100% { opacity: 0; }
  }
  .js .reveal.in .msg-typing--1 { animation: typing-pop 2.2s ease 2.2s both; }
  .js .reveal.in .msg-typing--2 { animation: typing-pop 2.7s ease 6.9s both; }

  /* viz 3 — iOS-style timezone picker (soft card) */
  .viz-tz { flex: 1; min-height: 0; position: relative; margin-top: 16px; }
  .tz-pick {
    position: absolute;
    inset: 0;
    background: var(--bg-card);
    border-radius: var(--mr-lg);
    overflow: hidden;
    /* transformed children can escape overflow+radius clipping (Safari);
       clip-path keeps the wheel inside the rounded card */
    clip-path: inset(0 round 16px);
    box-shadow: 0 14px 30px -18px color-mix(in srgb, var(--accent-ink) 30%, transparent);
    font-family: var(--sf);
  }
  .tz-list { list-style: none; margin: 0; padding: 0; will-change: transform; }
  .tz-list li {
    height: 34px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 22px;
    font-size: var(--fs-md);
    color: var(--imsg-ink-dark);
    opacity: 0.32;
    transform: scale(0.92);
  }
  .tz-list li em { font-style: normal; font-size: var(--fs-sm); color: var(--imsg-grey); }
  .tz-list li.is-sel { opacity: 1; transform: none; }
  .tz-band {
    position: absolute;
    left: 9px; right: 9px; top: calc(50% - 17px);
    height: 34px;
    background: var(--imsg-field);
    border-radius: var(--mr-sm);
    pointer-events: none;
    z-index: 1;
  }
  .tz-pick::before, .tz-pick::after {
    content: '';
    position: absolute;
    left: 0; right: 0;
    height: 58px;
    z-index: 2;
    pointer-events: none;
  }
  .tz-pick::before { top: 0; background: linear-gradient(180deg, var(--bg-card) 26%, color-mix(in srgb, var(--bg-card) 0%, transparent)); }
  .tz-pick::after { bottom: 0; background: linear-gradient(0deg, var(--bg-card) 26%, color-mix(in srgb, var(--bg-card) 0%, transparent)); }

  /* viz 4 — video-call window (bleeds off the card's right edge) */
  .viz-call { margin: auto 14px 0 -54px; font-family: var(--sf); }
  .call-win {
    background: var(--bg-card);
    border: 0.5px solid var(--hairline);
    border-radius: var(--mr-md);
    overflow: hidden;
    box-shadow: var(--elev-3);
  }
  .call-bar {
    display: flex;
    align-items: center;
    gap: 6px;
    background: var(--surface-dark);
    padding: 8px 11px;
  }
  .call-bar span { width: 9px; height: 9px; border-radius: var(--r-circle); }
  .call-bar span:nth-child(1) { background: var(--mac-red); }
  .call-bar span:nth-child(2) { background: var(--mac-amber); }
  .call-bar span:nth-child(3) { background: var(--mac-green); }
  .call-bar em {
    font-style: normal;
    flex: 1;
    text-align: center;
    font-size: var(--fs-xs);
    color: color-mix(in srgb, var(--on-accent) 75%, transparent);
    margin-right: 39px; /* balance the traffic lights so the title truly centres */
  }
  .call-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 5px;
    padding: 6px;
    background: var(--surface-dark-2);
  }
  .call-tile {
    position: relative;
    aspect-ratio: 4 / 3;
    border-radius: var(--mr-xs);
    overflow: hidden;
    display: grid;
    place-items: center;
    font-size: 1rem;
    font-weight: var(--fw-semibold);
    color: var(--accent-ink);
  }
  .call-tile img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
  .call-tile:nth-child(1) { background: var(--accent-200); }
  .call-tile:nth-child(2) { background: var(--tile-warm); }
  .call-tile:nth-child(3) { background: color-mix(in srgb, var(--accent-200) 52%, var(--white)); }
  .call-tile:nth-child(4) { background: var(--tile-clay); }
  .call-name {
    position: absolute;
    left: 5px; bottom: 5px;
    background: var(--scrim); /* photo scrim — neutral by intent */
    color: var(--on-accent);
    font-size: 0.58rem;
    font-weight: var(--fw-regular);
    padding: 2px 7px;
    border-radius: var(--mr-xs);
    z-index: 1;
  }
  .call-mic {
    position: absolute;
    right: 5px; bottom: 5px;
    width: 15px; height: 15px;
    border-radius: var(--r-circle);
    background: var(--scrim); /* photo scrim — neutral by intent */
    display: grid;
    place-items: center;
    color: var(--on-accent);
    z-index: 1;
  }
  .call-mic svg { display: block; }

  /* viz 5 — action items, list runs off the card's bottom edge (blue card).
     The white card is sized by its own rows and sits on the bottom edge, so
     the list fills it instead of trailing off into empty white. */
  .viz-actions {
    flex: 1;
    min-height: 218px;
    margin-bottom: -28px;
    overflow: hidden;
    /* The nine rows are ALWAYS taller than the box — that overflow is the
       joke, not a bug. Without size containment a flex: 1 item still reports
       its full max-content height as the flex container's intrinsic height, so
       once .bcard's height became content-driven (see .bcard's min-height) this
       one deliberately-cropped list would have set the height of every card in
       the bento: +21px at 800 and +35px at 1200, for a list nobody is meant to
       see the end of. Contained, it asks for its min-height and no more, and
       the rows still render and still get clipped. */
    contain: size;
    font-family: var(--sf);
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
  }
  .act-card {
    background: var(--bg-card);
    border-radius: var(--mr-md) var(--mr-md) 0 0;
    padding: 14px 16px 0;
    flex: 0 0 auto;
    box-sizing: border-box;
    box-shadow: var(--elev-3);
  }
  .act-head { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--muted); margin-bottom: 8px; }
  .act-row {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 7px 0;
    border-top: 1px solid var(--line);
    font-size: var(--fs-sm);
    color: var(--ink);
  }
  .act-lbl { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .act-row em { font-style: normal; font-size: var(--fs-xs); color: var(--muted); }
  .act-dot {
    width: 15px; height: 15px;
    border-radius: var(--r-circle);
    flex: none;
    border: 1.5px solid var(--hairline);
    display: grid;
    place-items: center;
    color: var(--on-accent);
  }
  .act-done .act-dot { background: var(--success); border-color: var(--success); }
  .act-done .act-lbl { color: var(--muted); }
  .act-dot svg { display: block; }
  /* pending rows tick themselves off one by one after the card reveals (delay in --td) */
  .act-tick .act-dot svg { opacity: 0; transform: scale(0.4); }
  @keyframes act-dot-tick { to { background: var(--success); border-color: var(--success); } }
  @keyframes act-check-pop { 60% { transform: scale(1.25); } to { opacity: 1; transform: scale(1); } }
  @keyframes act-lbl-mute { to { color: var(--muted); } }
  .js .reveal.in .act-tick .act-dot { animation: act-dot-tick 0.3s ease var(--td, 2s) both; }
  .js .reveal.in .act-tick .act-dot svg { animation: act-check-pop 0.35s var(--ease-spring) var(--td, 2s) both; }
  .js .reveal.in .act-tick .act-lbl { animation: act-lbl-mute 0.3s ease var(--td, 2s) both; }
  @media (prefers-reduced-motion: reduce) {
    .act-tick .act-dot, .act-tick .act-dot svg, .act-tick .act-lbl { animation: none !important; }
  }

  /* viz 6 — inbox window (keeps its bleed off the card's right edge; the
     card centres it vertically via .bcard--mid) */
  .viz-mail { margin: 0 -54px 0 14px; font-family: var(--sf); }
  .mail-win {
    background: var(--bg-card);
    border: 0.5px solid var(--hairline);
    border-radius: var(--mr-md);
    overflow: hidden;
    box-shadow: var(--elev-3);
  }
  .mail-tools { padding: 7px 10px; border-bottom: 1px solid color-mix(in srgb, var(--ink) 7%, transparent); }
  .mail-search {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    box-sizing: border-box;
    background: var(--gm-bar);
    border-radius: var(--r-pill);
    padding: 5px 12px;
    font-size: var(--fs-xs);
    color: var(--gm-icon);
  }
  .mail-search svg { display: block; }
  .gm-list { background: var(--mock-white); }
  .gm-row {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 10px 13px;
    font-size: var(--fs-xs);
    line-height: 1.3;
  }
  .gm-row + .gm-row { border-top: 1px solid color-mix(in srgb, var(--ink) 7%, transparent); }
  .gm-read { background: var(--gm-read); color: var(--gm-icon); }
  .gm-sender { flex: none; width: 86px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .gm-sub { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .gm-sub i { font-style: normal; color: var(--gm-meta); font-weight: var(--fw-regular); }
  .gm-date { flex: none; font-size: var(--fs-xs); color: var(--gm-meta); }
  .gm-unread { background: var(--mock-white); }
  .gm-unread .gm-sender, .gm-unread .gm-sub { font-weight: var(--fw-bold); color: var(--gmail-ink); }
  .gm-unread .gm-date { color: var(--gmail-ink); font-weight: var(--fw-bold); }
  .gm-clip { flex: none; color: var(--gm-icon); line-height: 0; }

  /* The card internals have no entrance: hub tiles, windows and rows sit at
     their resting position from the first paint. What follows is a different
     thing — the mock PLAYING, not the content arriving. */
  /* conversation playback (client POV): typing…, msg, delivered, typing…, reply, …
     The bubbles are ANIMATIONS, never transitions, and that is the whole point.
     Everything else in this thread — typing dots, Delivered/Read, the tapback —
     is a CSS animation, and .anim-pause freezes animation-play-state while the
     section is off screen while doing nothing at all to a transition. On
     transitions the bubbles kept running through a scroll-away and came back
     out of register with their own receipts. Same mechanism = one clock.
     Two keyframes because the rest transform differs by direction; note there
     is deliberately NO Y offset on a bubble, only the sideways slide.
     The reveal stays one-shot on purpose (the observer unobserves), so an
     interrupted playback resumes where it paused rather than re-arming. */
  @keyframes msg-b-in { from { opacity: 0; transform: translateX(-14px); } to { opacity: 1; transform: none; } }
  @keyframes msg-b-out { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: none; } }
  .js .reveal .msg-b { opacity: 0; }
  .js .reveal.in .msg-row--in .msg-b { animation: msg-b-in 0.5s var(--ease-out) both; }
  .js .reveal.in .msg-row--out .msg-b { animation: msg-b-out 0.5s var(--ease-out) both; }
  .js .reveal.in .viz-msg > div:nth-child(2) .msg-b { animation-delay: 0.6s; }
  .js .reveal.in .viz-msg > div:nth-child(4) .msg-b { animation-delay: 4.4s; }
  .js .reveal.in .viz-msg > div:nth-child(5) .msg-b { animation-delay: 5.2s; }
  .js .reveal.in .viz-msg > div:nth-child(7) .msg-b { animation-delay: 9.6s; }
  .js .reveal.in .viz-msg > div:nth-child(8) .msg-b { animation-delay: 10.4s; }

  @media (prefers-reduced-motion: reduce) {
    .bcard { transition: none; }
    .bcard:hover { transform: none; }
    /* .msg-b left this group when the bubbles moved from transitions to
       keyframes: `transition: none` no longer reaches them, so kill the
       animation instead and show every bubble at its resting position. */
    .js .reveal .msg-b { animation: none !important; opacity: 1 !important; transform: none !important; }
    /* fixed picker height so the static Brisbane row sits exactly in the band */
    .viz-tz { flex: none; height: 238px; min-height: 238px; }
    /* no travelling pulses; static fanned deck (JS shuffle is also skipped) */
    .pulse { animation: none !important; opacity: 0 !important; }
    .tcard { transition: none !important; }
    .msg-typing { animation: none !important; opacity: 0 !important; }
    .msg-st-delivered { animation: none !important; opacity: 0 !important; }
    .msg-st-read { animation: none !important; opacity: 1 !important; }
    /* static end-state: last receipt reads Read, the earlier two are hidden.
       No height override any more — the 14px slot is reserved on every row in
       the base rule, so the reduced-motion thread is the exact same box as the
       animated one once it has finished playing. */
    .msg-status--1, .msg-status--2 { animation: none !important; opacity: 0 !important; }
    .msg-status--3 { animation: none !important; opacity: 1 !important; }
    .js .reveal .msg-react { animation: none !important; opacity: 1 !important; }
    .msg-row--react { animation: none !important; margin-top: 26px !important; }
  }

  /* Problem — static collage: the claim sits in a big card, benefits fan out around it */
  /* the problem band: off-white ground. The collage cards are product mocks —
     white cards floating on the soft band. */
  .problem { background: var(--bg-soft); overflow: hidden; }
  /* mobile-first: centre tile on top, cards in a simple grid below */
  .pcollage {
    position: relative;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--sp-4);
  }
  /* the claim sits on its own sheet, and that sheet is the DARK one — the same
     --accent-ink panel the Services .callbox uses, asked for on 28 Aug. Its
     history, because it has been churned: it was a white sheet, was stripped
     to bare ground for a few hours that morning, was put back white at the
     client's request, and went navy that evening. Do not strip the background
     from here again without asking him first — the sheet itself is settled,
     only its colour moved.
     Going dark took the border and the shadow with it, and that was not an
     efficiency. --line is a light-grey hairline drawn to separate a
     WHITE sheet from warm paper, and on navy it reads as a scuff round the
     edge rather than an edge. --shadow is likewise a lift-off-paper cue for a
     pale sheet; against a panel this dark the value step does all the
     separating on its own and the shadow only muddies the corners. If the
     sheet ever goes light again, both come back with it.
     The whole type stack had to move to the on-dark set at the same time:
     --accent-text is the LIGHT-ground blue (2.11:1 on this ink) and
     --accent-ondark is the dark-ground one (1.28:1 on paper). Neither survives
     on the other ground — they are not interchangeable, they are a pair.
     The padding stays — it is what holds the copy clear of the cards' bite. */
  .pc-center {
    position: relative;
    z-index: 1;
    order: -1;
    grid-column: 1 / -1;
    background: var(--accent-ink);
    border-radius: var(--radius-card);
    padding: clamp(44px, 5.5vw, 72px) clamp(28px, 4vw, 56px);
    text-align: center;
  }
  /* the same 48x4 accent block that opens .tools-copy, in the on-dark blue and
     centred over the headline because this card's content is centred. The
     bottom margin runs tighter than the tools one (which grows to 48px): on
     desktop this card is a fixed aspect-ratio sheet, so every pixel the rule
     adds is taken out of the clear space above the headline. */
  .pc-center::before {
    content: '';
    display: block;
    width: 48px;
    height: 4px;
    background: var(--accent-ondark);
    margin: 0 auto clamp(20px, 2vw, 28px);
  }
  .pc-center h2 { font-size: clamp(1.7rem, 2.5vw, 2.3rem); text-wrap: balance; color: var(--accent-ondark); margin-inline: auto; }
  .pc-center p { margin-top: var(--sp-4); color: var(--ink-ondark); font-size: var(--fs-md); line-height: 1.4; max-width: 560px; margin-inline: auto; }
  /* the bold run inside the standfirst: --ink was body-ink on white and is
     invisible here. --white is the one step brighter than --ink-ondark that
     still reads as emphasis rather than a second colour. */
  .pc-center strong { color: var(--white); font-weight: var(--fw-semibold); }
  /* the card's CTA. justify-self keeps it centred once the desktop rule turns
     this card into a grid (auto margins would centre a block, but a grid item
     stretches instead), and it is harmless on the mobile block layout.
     CLICKABILITY, not decoration: on desktop this card sits at z-index 0 and
     the white .pcards pass OVER it at 2-4, so anything interactive here has to
     be verified clear of them, not assumed. It is - measured with
     elementFromPoint across the button's whole rect at 1920/1440/1280/1024/901
     and 390, corners and centre, and the anchor comes back every time. The
     cards bite the card's left and right flanks at the HEADLINE's height; the
     button sits below the standfirst, in the clear band under that bite, and
     it is a narrow centred box (~186px in a 400-624px sheet).
     Raising z-index here would NOT be the fix if that ever changes: .pc-center
     has its own z-index, so it is a stacking context and no descendant can
     climb out of it. The fix would have to be at the card's own level, which
     means reversing the settled composition - ask first. */
  .pc-center .btn { margin-top: var(--space-cta); justify-self: center; }

  .pcard {
    position: relative;
    background: var(--bg-card);
    border: var(--rule-hairline) solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow);
    padding: 18px var(--sp-5);
  }
  .pcard-title { font-size: var(--fs-base); font-weight: var(--fw-semibold); letter-spacing: -0.01em; line-height: 1.35; }
  /* vignette slot — mockups render in the system font, HIG style; dashed placeholder while empty */
  .pv { margin-top: 12px; font-family: var(--sf); }
  .pv:empty { min-height: 64px; border-radius: var(--mr-sm); background: var(--bg); border: 1px dashed var(--hairline); }

  /* vignette: LTV bar chart (card 4) — Health-app anatomy: summary header above the data,
     quiet gridlines with trailing-side tick labels, one series color, noisy upward data */
  .pv-ltv-head { display: grid; gap: 2px; padding: 2px 0 10px; }
  .pv-ltv-kicker {
    font-size: 0.55rem;
    font-weight: var(--fw-semibold);
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--muted);
  }
  .pv-ltv-range { font-size: var(--fs-2xs); color: var(--muted); }
  .pv-ltv-statrow { display: flex; align-items: baseline; gap: 8px; margin-top: 2px; }
  .pv-ltv-big { font-size: var(--fs-lg); font-weight: var(--fw-semibold); letter-spacing: -0.02em; color: var(--ink); line-height: 1; }
  .pv-ltv-badge { font-size: var(--fs-2xs); font-weight: var(--fw-medium); color: var(--muted); }
  .pv-ltv-badge b { color: var(--accent); font-weight: var(--fw-bold); margin-right: 2px; }
  .pv-ltv-frame {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 7px;
  }
  .pv-ltv-plot {
    position: relative;
    display: flex;
    align-items: flex-end;
    gap: 3px;
    height: 88px;
    border-bottom: 1px solid var(--hairline);
    /* gridlines at the $60 and $120 ticks only */
    background-image:
      linear-gradient(color-mix(in srgb, var(--ink) 7%, transparent), color-mix(in srgb, var(--ink) 7%, transparent)),
      linear-gradient(color-mix(in srgb, var(--ink) 7%, transparent), color-mix(in srgb, var(--ink) 7%, transparent));
    background-repeat: no-repeat;
    background-size: 100% 1px, 100% 1px;
    background-position: 0 0, 0 50%;
  }
  .pv-ltv-plot i {
    flex: 1;
    height: var(--h);
    background: var(--accent);
    border-radius: 2px 2px 0 0;
  }
  /* dashed average line, HIG Screen-Time style, with its little tag riding the line */
  .pv-ltv-avg {
    position: absolute;
    left: 0;
    right: 0;
    bottom: var(--avg);
    border-top: 1.5px dashed var(--accent-ink);
    opacity: 0.85;
  }
  .pv-ltv-avg em {
    position: absolute;
    right: 2px;
    bottom: 2px;
    font-style: normal;
    font-size: 0.5rem;
    font-weight: var(--fw-semibold);
    color: var(--accent-ink);
    background: var(--bg-card);
    padding: 0 3px;
    border-radius: 3px;
  }
  .pv-ltv-plot svg { position: absolute; inset: 0; width: 100%; height: 100%; }
  .pv-ltv-line { stroke: var(--accent); stroke-width: 1.6; stroke-linejoin: round; stroke-linecap: round; }
  .pv-ltv-plot circle { fill: var(--accent); }
  .pv-ltv-dot { fill: var(--bg-card); stroke: var(--accent); stroke-width: 2; }
  /* lollipop annotation: date pill over bold value, centred on the stem */
  .pv-ltv-lolli {
    position: absolute;
    top: -14px;
    right: -6px;
    display: grid;
    justify-items: center;
    gap: 1px;
    text-align: center;
  }
  .pv-ltv-lolli i {
    font-style: normal;
    font-size: 0.5rem;
    color: var(--muted);
    background: color-mix(in srgb, var(--ink) 8%, transparent);
    padding: 1px 5px;
    border-radius: var(--mr-xs);
    white-space: nowrap;
  }
  .pv-ltv-lolli b { font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--ink); line-height: 1.1; }
  .pv-ltv-y {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    height: 88px;
    transform: translateY(0.35em);
  }
  .pv-ltv-y span { font-size: 0.55rem; color: var(--muted); text-align: left; line-height: 1; }
  .pv-ltv-x { display: flex; justify-content: space-between; padding-top: 4px; }
  .pv-ltv-x span { font-size: 0.55rem; color: var(--muted); }
  .pv-ltv-x span:nth-child(2) { text-align: center; }
  /* one-shot reveal: bars grow / line draws, dots pop, lollipop lands, stats fade in */
  .js .problem .pv-ltv-plot i { height: 4%; transition: height 0.7s var(--ease-out); transition-delay: var(--d, 0s); }
  .js .problem.play .pv-ltv-plot i { height: var(--h); }
  .js .problem .pv-ltv-avg { transform: scaleX(0); transform-origin: left; transition: transform 0.7s var(--ease-standard) 1.5s; }
  .js .problem.play .pv-ltv-avg { transform: scaleX(1); }
  .js .problem .pv-ltv-line { stroke-dasharray: 400; stroke-dashoffset: 400; transition: stroke-dashoffset 1.2s ease-out 0.55s; }
  .js .problem.play .pv-ltv-line { stroke-dashoffset: 0; }
  .js .problem .pv-ltv-plot circle { opacity: 0; transition: opacity 0.3s ease var(--d, 1.3s); }
  .js .problem.play .pv-ltv-plot circle { opacity: 1; }
  .js .problem.play .pv-ltv-lolli { opacity: 1; }
  .js .problem .pv-ltv-statrow { opacity: 0; transition: opacity 0.5s ease 2.1s; }
  .js .problem.play .pv-ltv-statrow { opacity: 1; }

  /* the centre-tile headline: plain two-line display type, no marker sweep */
  .hl { position: relative; display: block; }
  .hl-base { display: block; }
  .hl-pad { display: block; }
  /* the headline holds exactly two lines at every width */
  .hl-base span { white-space: nowrap; }
  .problem .hl { font-size: clamp(1.15rem, 4.6vw, 2.6rem); }
  /* 641–900px: lifted alongside desktop, but by slope and cap separately, so
     the step at the 640/641 breakpoint stays small (the <=640 rule caps at
     2rem and is not being raised). Measured ink width of the longer line:
     ~362px in a 537px content box at 641, ~499px in a 780px box at 900. */
  @media (max-width: 900px) { .problem .hl { font-size: clamp(1.5rem, 5.9vw, 3.25rem); } }
  /* NOT lifted: at <=640 the nowrap headline is already close to the gutters —
     254px of ink in a 284px content box at a 390px viewport — and .problem is
     overflow:hidden, so any overgrowth is cut off silently rather than
     scrolling. Raise nothing in this rule without re-measuring at 360px. */
  @media (max-width: 640px) { .problem .hl { font-size: clamp(1.5rem, 6.9vw, 2rem); } }
  @media (prefers-reduced-motion: reduce) {
    .js .problem .pv-ltv-plot i { height: var(--h); transition: none; }
    .js .problem .pv-ltv-avg { transform: none; transition: none; }
    .js .problem .pv-ltv-line { stroke-dashoffset: 0; transition: none; }
    .js .problem .pv-ltv-statrow { opacity: 1; transition: none; }
    .js .problem .pv-notif-banner { opacity: 1; transform: none; transition: none; }
    .js .problem .pv-opt--sel { background: color-mix(in srgb, var(--accent) 9%, var(--white)); border-color: var(--accent); transition: none; }
    .js .problem .pv-opt--sel .pv-radio { border: 4.5px solid var(--accent); transition: none; }
    .js .problem .pv-cursor { opacity: 1; transform: none; transition: none; }
    .js .problem .pv-order { opacity: 1; transform: none; transition: none; }
  }

  /* vignette: push notification (card 2) — iOS banner on a mint "wallpaper" panel */
  .pv-notif {
    background: linear-gradient(160deg, color-mix(in srgb, var(--accent-200) 55%, var(--white)), var(--accent-200));
    border-radius: var(--mr-md);
    padding: 14px 12px;
  }
  .pv-notif-banner {
    display: flex;
    align-items: center;
    gap: 10px;
    background: color-mix(in srgb, var(--bg-card) 82%, transparent);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    border-radius: var(--mr-lg);
    padding: 10px 12px;
    box-shadow: 0 6px 16px -8px color-mix(in srgb, var(--accent-ink) 30%, transparent);
  }
  .pv-notif-icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    border-radius: var(--mr-sm);
    background: var(--bg-card);
    border: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
  }
  .pv-notif-icon svg { width: 21px; height: 16px; }
  .pv-notif-txt { min-width: 0; display: grid; gap: 0; flex: 1; }
  .pv-notif-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
  .pv-notif-row b { font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--ink); line-height: 1.25; letter-spacing: -0.01em; }
  .pv-notif-row em { font-style: normal; font-size: var(--fs-2xs); color: var(--muted); }
  .pv-notif-subject { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--ink); line-height: 1.25; letter-spacing: -0.01em; }
  .pv-notif-body { font-size: var(--fs-xs); color: var(--muted); line-height: 1.25; letter-spacing: -0.01em; }
  /* banner drops in once, iOS style */
  .js .problem .pv-notif-banner { opacity: 0; transform: translateY(-14px) scale(0.96); transition: opacity 0.55s var(--ease-spring) 0.9s, transform 0.55s var(--ease-spring) 0.9s; }
  .js .problem.play .pv-notif-banner { opacity: 1; transform: none; }

  /* vignette: welcome-offer chooser (card 3) — bundle option selected, cursor hovering */
  .pv-choice { display: grid; gap: 7px; }
  .pv-opt {
    position: relative;
    display: flex;
    align-items: center;
    gap: 8px;
    background: var(--bg-card);
    border: 1px solid var(--hairline);
    border-radius: var(--mr-sm);
    padding: 8px 10px;
    font-size: var(--fs-xs);
    font-weight: var(--fw-medium);
    color: var(--ink);
    line-height: 1.3;
  }
  .pv-radio { flex: none; width: 15px; height: 15px; border-radius: var(--r-circle); border: 1.5px solid var(--hairline); background: var(--bg-card); box-sizing: border-box; }
  .pv-opt--sel { background: color-mix(in srgb, var(--accent) 9%, var(--white)); border-color: var(--accent); }
  .pv-opt--sel .pv-radio { border: 4.5px solid var(--accent); }
  .pv-cursor {
    position: absolute;
    right: 12px;
    bottom: -7px;
    width: 17px;
    height: 17px;
    filter: drop-shadow(0 2px 3px color-mix(in srgb, var(--accent-ink) 35%, transparent));
  }
  /* the pick happens on reveal: option 2 flips selected, cursor slides in */
  .js .problem .pv-opt--sel { background: var(--bg-card); border-color: var(--hairline); transition: background 0.3s ease 1.35s, border-color 0.3s ease 1.35s; }
  .js .problem .pv-opt--sel .pv-radio { border: 1.5px solid var(--hairline); transition: border 0.25s ease 1.35s; }
  .js .problem .pv-cursor { opacity: 0; transform: translate(10px, 8px); transition: opacity 0.4s ease 0.95s, transform 0.4s var(--ease-out) 0.95s; }
  .js .problem.play .pv-opt--sel { background: color-mix(in srgb, var(--accent) 9%, var(--white)); border-color: var(--accent); }
  .js .problem.play .pv-opt--sel .pv-radio { border: 4.5px solid var(--accent); }
  .js .problem.play .pv-cursor { opacity: 1; transform: none; }

  /* vignette: order feed (card 5) — repeat orders ticking in */
  .pv-orders { display: grid; gap: 6px; }
  .pv-order {
    display: flex;
    align-items: center;
    gap: 8px;
    background: var(--bg-card);
    border: 1px solid var(--hairline);
    border-radius: var(--mr-sm);
    padding: 7px 10px;
  }
  .pv-order i { flex: none; width: 7px; height: 7px; border-radius: var(--r-circle); background: var(--accent); }
  .pv-order-txt { flex: 1; display: flex; align-items: baseline; gap: 8px; min-width: 0; }
  .pv-order-txt b { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--ink); }
  .pv-order-txt em { font-style: normal; font-size: var(--fs-2xs); color: var(--muted); }
  .pv-order-time { font-size: 0.58rem; color: var(--muted); white-space: nowrap; }
  .js .problem .pv-order { opacity: 0; transform: translateY(6px); transition: opacity 0.4s ease var(--d, 0.7s), transform 0.4s ease var(--d, 0.7s); }
  .js .problem.play .pv-order { opacity: 1; transform: none; }

  /* vignette: Klaviyo flow list (card 6) — two live abandonment flows */
  .pv-flows { display: grid; }
  .pv-flow { display: grid; gap: 3px; padding: 8px 2px; }
  .pv-flow + .pv-flow { border-top: 1px solid var(--line); }
  .pv-flow-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
  .pv-flow-top b { font-size: var(--fs-2xs); font-weight: var(--fw-semibold); color: var(--accent-deep); }
  .pv-flow-live {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 0.56rem;
    font-weight: var(--fw-semibold);
    color: var(--success-deep);
    background: var(--success-soft);
    border-radius: var(--radius-chip);
    padding: 1px 7px;
  }
  .pv-flow-live i { width: 5px; height: 5px; border-radius: var(--r-circle); background: var(--success); }
  .pv-flow-sub { display: flex; align-items: center; gap: 5px; font-size: var(--fs-2xs); color: var(--muted); white-space: nowrap; }
  .pv-flow-sub svg { width: 11px; height: 11px; flex: none; }
  .pv-flow-up { margin-left: auto; font-size: var(--fs-2xs); font-weight: var(--fw-semibold); color: var(--success-deep); }

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

  /* desktop: cards ring the tile — corners, sides, top and bottom — deliberately uneven.

     Geometry is ported from the original collage. Measured off that artwork
     (canvas 1582 wide) and scaled by 0.80 to this grid, every element is a
     fixed share of the collage's 1264px visual width:

       tile        693 x 466   (54.8% of the collage, aspect 1.487)
       cards 1/3/4/6   318     (25.1% each)
       card 2          387     (30.6% — the one deliberately wider card)
       card 5          316

     Cards clear the tile except for a shallow bite out of its edge: the two
     mid cards (2 and 5) cross it by ~70px and ~62px over their full height,
     the four corner cards only nip a corner. A card's bite = its negative
     margin minus the 40px of slack between the tile and its track. */
  @media (min-width: 901px) {
    .pcollage {
      /* Every horizontal measure is a share of the viewport, so the collage
         holds the proportions above at any desktop width. The px term in each
         min() freezes growth once .container reaches its 1856px cap. */
      --pc-side:  min(17.3125vw, 338px);
      --pc-mid:   min(48.3125vw, 943px);
      --pc-tile:  min(43.3125vw, 845px);
      --pc-card:  min(19.875vw,  388px);
      --pc-card2: min(24.1875vw, 472px);
      --pc-card5: min(19.75vw,   386px);
      /* how far each card crosses its track edge; the bite it takes out of the
         tile is this minus the 40px-equivalent slack either side of the tile */
      --pc-m1: min(6.6875vw,  130px);
      --pc-m2: min(6.875vw,   134px);
      --pc-m3: min(10.4375vw, 204px);
      --pc-m4: min(10.4375vw, 204px);
      --pc-m5: min(6.375vw,   124px);
      --pc-m6: min(6.5625vw,  128px);
      grid-template-columns: var(--pc-side) minmax(0, var(--pc-mid)) var(--pc-side);
      grid-template-rows: auto auto auto;
      align-items: start;
      justify-items: center;
      justify-content: center;
      gap: 0;
      padding: var(--sp-10) 0;
    }
    /* the tile spans all three rows so it stays centred against the ring of
       cards. align-self:center stops it stretching to the ring's full height,
       and the aspect-ratio then makes it a tall floating sheet rather than a
       box that hugs its text — the empty space above the headline and below
       the standfirst is the point. */
    .pc-center { order: 0; grid-area: 1 / 2 / 4 / 3; z-index: 0; align-self: center; justify-self: center; display: grid; align-content: center; width: 100%; max-width: var(--pc-tile); aspect-ratio: 693 / 466; margin: 105px auto 0; padding-block: clamp(24px, 2.6vw, 40px); padding-inline: clamp(24px, 3vw, 44px); }
    /* .hl-pad is nowrap, so the headline is one unbreakable block whose width
       is set purely by font-size. Left at a fixed 2.6rem cap it stays ~396px
       wide while the tile scales with the viewport, and below ~1150px it
       outgrows the tile and the side cards clip it. Scale it on the same vw
       basis as --pc-tile and it stays a constant 70% of the tile's width at
       every desktop size; the standfirst is pinned to the same basis.
       What limits the size is NOT the tile's padding box: cards 2 and 5 bite
       ~84px and ~74px into the tile at exactly the headline's height and sit
       above it (z-index 2 vs 0), so the genuinely clear window is ~35.1vw,
       not the ~38.7vw content box. At 3.15vw the longer line measures ~30.2vw
       (~580px at 1920, up from ~476px at 2.6vw), leaving ~2.2vw on the left
       and ~2.7vw on the right — about 42px and 51px at 1920, 21px and 26px at
       a 901px window. Past ~3.6vw the headline slides under the cards and is
       silently clipped — the sheet is there (navy now), but it sits BELOW them
       (z-index 0 vs 2), so it does not stop or reveal that overlap either.
       Do not chase the last vw. */
    .pc-center h2 { max-width: none; }
    .pc-center .hl { font-size: min(3.15vw, 61.45px); }
    .pc-center p { max-width: min(26.875vw, 524px); }
    /* the cards sit clear of the tile bar a shallow bite, and align to the top
       of their row so each one's vertical offset is set outright rather than
       inherited from whatever the other column's card happens to measure */
    .pcard { width: var(--pc-card); max-width: none; }
    .pcard--1 { grid-area: 1 / 1; z-index: 2; justify-self: end; margin: 16px calc(var(--pc-m1) * -1) 0 0; }
    .pcard--2 { grid-area: 2 / 1; z-index: 2; justify-self: end; width: var(--pc-card2); margin: 107px calc(var(--pc-m2) * -1) 0 0; }
    .pcard--3 { grid-area: 3 / 1; z-index: 2; justify-self: end; margin: 49px calc(var(--pc-m3) * -1) 0 0; }
    .pcard--4 { grid-area: 1 / 3; z-index: 2; justify-self: start; margin: 0 0 0 calc(var(--pc-m4) * -1); }
    .pcard--5 { grid-area: 2 / 3; z-index: 3; justify-self: start; width: var(--pc-card5); margin: 158px 0 0 calc(var(--pc-m5) * -1); }
    .pcard--6 { grid-area: 3 / 3; z-index: 4; justify-self: start; margin: 57px 0 0 calc(var(--pc-m6) * -1); }
  }

  /* Who We Work With — a signpost list on the page's own paper ground.
     Left: the pitch and one CTA. Right: six hairline-ruled rows. */
  .niches-split {
    display: grid;
    grid-template-columns: minmax(0, 34fr) minmax(0, 66fr);
    gap: 0 clamp(var(--gap-grid), 5vw, 96px);
    align-items: start;
  }
  /* the pitch column is a dark panel, the same --accent-ink block and the same
     padding as the Services .callbox — one dark-panel treatment on the site,
     not two. Its CTA carries .btn--ondark in the markup for the same reason:
     that modifier is the callbox's vocabulary, and pulling `.niches-head .btn`
     into the .btn--ondark selector list would teach a utility about a section.
     The old max-width:42ch went with the panel. 42ch was a MEASURE cap, right
     for bare copy sitting on paper where the line length is the only edge the
     reader sees. A panel has a real edge, and a real edge that stops short of
     its column for no visible reason reads as a mistake — so the panel fills
     the column and the 34/66 split does the measuring instead. */
  .niches-head {
    background: var(--accent-ink);
    padding: clamp(28px, 2.4vw, 40px);
  }
  .niches-head .eyebrow { color: var(--label-ondark); margin-bottom: var(--space-eyebrow); }
  .niches-head h2 { font-size: clamp(2.125rem, 3.2vw, 3.25rem); color: var(--white); }
  .niches-head p { margin-top: var(--space-stack); color: var(--ink-ondark); font-size: var(--fs-lede); line-height: 1.4; }
  /* .btn is white-space:nowrap, and this label is the site's longest at ~321px
     with its padding. Loose on paper that never showed: the block had no edge,
     so the label just ran past the 42ch measure. Inside a padded panel it is
     a hard failure twice over — the left column is only ~298px at 1024, so the
     label crossed the panel's right edge and finished on paper in
     --accent-ondark (1.28:1 there); and at <=900 the single 1fr track resolves
     to min-content, so the panel itself blew 35px past the container. Letting
     this one label wrap fixes both without touching the track or the copy. It
     still sets on one line from ~1400 up, where the column can hold it. */
  .niches-head .btn { margin-top: 38px; white-space: normal; }
  .niche-list { list-style: none; border-bottom: var(--rule-hairline) solid var(--rule); }
  .niche-row {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 30px;
    gap: 0 var(--sp-10);
    align-items: start;
    border-top: var(--rule-hairline) solid var(--rule);
    padding: var(--sp-9) 20px;
    margin-inline: -20px;
    width: calc(100% + 40px);
    text-decoration: none;
    transition: background-color var(--dur-base) ease;
  }
  .niche-row:hover { background: var(--wash); }
  .niche-name {
    display: block;
    font-family: var(--serif);
    font-weight: var(--fw-display);
    font-size: clamp(1.6rem, 2.4vw, 2.75rem);
    line-height: 1.14;
    letter-spacing: var(--tracking-display);
    color: var(--accent-ink);
  }
  .niche-desc { display: block; margin-top: 14px; color: var(--muted); font-size: var(--fs-base); max-width: 52ch; }
  .niche-arrow {
    grid-column: 2;
    grid-row: 1;
    width: 30px; height: 10px;
    justify-self: end;
    margin-top: 0.6em;
    overflow: visible;
    transition: transform 0.34s var(--ease-out);
  }
  .niche-arrow path { stroke: var(--accent); stroke-width: 1; fill: none; }
  .niche-row:hover .niche-arrow { transform: translateX(8px); }
  /* the overflow industries stay, as a quiet full-width tail under the split */
  .niche-chips {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: var(--space-block);
  }
  .niche-chips li {
    padding: 7px var(--sp-4);
    border-radius: var(--r-chip);
    background: var(--wash);
    border: var(--rule-hairline) solid var(--rule);
    color: var(--accent-ink);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
  }
  .niches .fit-grid { margin-top: var(--sp-18); }
  /* entrance: rows rise in with the section, staggered */
  .js .niches .niche-row { opacity: 0; transform: translateY(14px); transition: opacity 0.5s ease, transform 0.55s var(--ease-out); }
  .js .niches.in .niche-row { opacity: 1; transform: none; }
  .js .niches.in .niche-row:nth-child(2) { transition-delay: 0.07s; }
  .js .niches.in .niche-row:nth-child(3) { transition-delay: 0.14s; }
  .js .niches.in .niche-row:nth-child(4) { transition-delay: 0.21s; }
  .js .niches.in .niche-row:nth-child(5) { transition-delay: 0.28s; }
  .js .niches.in .niche-row:nth-child(6) { transition-delay: 0.35s; }
  @media (prefers-reduced-motion: reduce) {
    .js .niches .niche-row { opacity: 1; transform: none; transition: none; }
  }
  @media (max-width: 900px) {
    .niches-split { grid-template-columns: 1fr; gap: var(--gap-grid-tight); }
    .niche-row { padding: var(--sp-7) 16px; margin-inline: -16px; width: calc(100% + 32px); }
  }

  /* utilities */
  .section--tight { padding-top: var(--sp-10); }
  /* Right-justified section header. Was .process-header--center (centred) and
     is used by the FAQ alone, so the rule was turned rather than duplicated —
     the name follows the behaviour. margin-left:auto pushes the 820px measure
     of .process-header to the container's right edge; margin-right must be 0,
     not auto, or the block re-centres. */
  .process-header--right { text-align: right; margin: 0 0 70px auto; }

  /* Section headers (shared) */
  .process-header { max-width: 820px; margin-bottom: 70px; }
  .process-header p { margin-top: var(--space-stack); color: var(--muted); font-size: var(--fs-lede); line-height: 1.4; }

  /* Process — three months, minimal columns (the services rows' language turned
     vertical), thin arrows in the gutters between them */
  #process { overflow-x: clip; }
  .months { display: grid; grid-template-columns: 1fr; }
  /* each month is a full-width row: media one side, copy the other, and the
     side alternates down the stack. Month 1 reads top, month 3 bottom. */
  /* text in a narrow column, the photo taking most of the frame; the photo
     swaps sides down the stack */
  .month-card {
    display: grid;
    grid-template-columns: minmax(0, 34fr) minmax(0, 66fr);
    gap: 0 clamp(40px, 4vw, 72px);
    align-items: center;
    border-top: var(--rule-hairline) solid var(--rule);
    padding-top: clamp(36px, 3.4vw, 56px);
  }
  /* DOM order is media-then-copy, so the default row pushes the media to
     column 2 (the wide one) and the copy takes the narrow column on the left */
  .month-media { order: 2; margin-right: calc(-1 * (var(--gutter, 48px) + max(0px, (100vw - 1856px) / 2))); }
  .month-card--flip .month-media { margin-right: 0; margin-left: calc(-1 * (var(--gutter, 48px) + max(0px, (100vw - 1856px) / 2))); }
  .month-card--flip { grid-template-columns: minmax(0, 66fr) minmax(0, 34fr); }
  .month-card--flip .month-media { order: 0; }
  /* the media plate: a flat blue field today, an <img> the moment one lands.
     The clip lives on <picture>, NOT on .month-media: the pop bands have to
     straddle the plate's inner edge, and a clip here would shear them off. */
  .month-media { position: relative; aspect-ratio: 3 / 2; background: var(--bg-soft); }
  .month-media picture { display: block; width: 100%; height: 100%; overflow: hidden; }
  .month-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
  /* The pop bands — the A&O offset composition. TWO solid slabs per photo,
     never three: scarcity is what makes them read as architecture rather
     than confetti. Both hang off the SAME inner vertical edge and both are
     large (a third to a half of the plate's width), so the pair reads as one
     aligned mass instead of two loose chips. Inner, not outer, because the
     outer edge is the full-bleed one that runs off the viewport, and a block
     there would simply be cut in half by the window. Every horizontal edge
     they sit on already exists in the layout — nothing floats at an
     arbitrary percentage: band A's top edge IS the photo's top edge, band
     B's bottom edge IS the photo's bottom edge, which is also the baseline
     the .month-plate numeral already sits on, so the whole composition —
     both bands and the numeral — lines up on edges the layout already has.
     The overhang is capped below the grid's minimum 40px gutter so neither
     band can reach into the copy column and sit on the text. One flat
     colour per composition: a second tint is exactly the fussiness this is
     meant to avoid.

     Band B and the numeral share the photo's bottom-INNER corner on a
     default card, and that works only because --bar there is the same
     --accent-ink as the plate: the two merge into a single slab with the
     numeral set inside it. So the --pop card must stay flipped, which puts
     its pink bands on the right edge and the navy plate alone on the left —
     a --pop card in the default orientation would drop a navy plate onto a
     pink band and read as a collision. Tried and rejected: standing band B
     on the numeral's TOP edge instead, which turns the pair into a stepped
     L and makes the numeral look like an accidental notch. */
  .month-card { --bar: var(--accent-ink); --bar-overhang: clamp(18px, 1.8vw, 32px); }
  .month-card--pop { --bar: var(--pop-pink); }
  .month-bar {
    position: absolute;
    z-index: 1;                 /* over the photo, under the .month-plate numeral */
    background: var(--bar);
    pointer-events: none;
  }
  /* sizing stays percentage-based: the plate spans a 34fr column at desktop
     but goes full width below 900px, so px offsets would bunch up */
  /* band A — the wide slab, top flush with the photo's top edge */
  .month-bar--a { top: 0; width: clamp(150px, 42%, 500px); height: clamp(52px, 16%, 132px); }
  /* band B — the shorter slab, bottom flush with the photo's bottom edge */
  .month-bar--b { bottom: 0; width: clamp(120px, 30%, 360px); height: clamp(42px, 13%, 104px); }
  /* default card: media is the RIGHT column, so its inner edge is the left one */
  .month-bar { left: 0; transform: translateX(calc(-1 * var(--bar-overhang))); }
  /* flipped card: media is the LEFT column — the blocks swap to the right edge */
  .month-card--flip .month-bar { left: auto; right: 0; transform: translateX(var(--bar-overhang)); }
  /* the month numeral rides the photo as a solid navy tag, bottom-left —
     a filled plate so the numeral never lands on a light part of the duotone */
  .month-plate {
    position: absolute;
    left: 0;
    bottom: 0;
    z-index: 2;
    padding: 4px clamp(18px, 1.6vw, 26px) 10px;
    background: var(--accent-ink);
    font-family: var(--serif);
    font-weight: var(--fw-display);
    font-size: clamp(2.4rem, 4vw, 4rem);
    line-height: 1;
    letter-spacing: var(--tracking-display);
    color: var(--accent-ondark);
  }
  .month-copy { padding: clamp(8px, 2vw, 32px) 0; }
  .month-num {
    display: block;
    color: var(--accent-text);
    font-weight: var(--fw-semibold);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.2em;
  }
  .month-title {
    font-family: var(--serif);
    font-size: clamp(1.9rem, 3vw, 2.9rem);
    font-weight: var(--fw-display);
    letter-spacing: var(--tracking-display);
    line-height: 1.08;
    color: var(--accent-ink);
    margin: 14px 0 18px;
  }
  .month-card p { color: var(--muted); font-size: var(--fs-lede); max-width: 46ch; line-height: 1.4; }
  .month-arrow { /* the same hairline arrow the services rows use, turned down the stack */
    width: 42px;
    height: 14px;
    margin: clamp(36px, 3.4vw, 56px) auto;
    transform: rotate(90deg);
    background: var(--accent);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 10' fill='none'%3E%3Cpath d='M0 5h28M23.5 1 28 5l-4.5 4' stroke='black' stroke-width='1' fill='none'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 10' fill='none'%3E%3Cpath d='M0 5h28M23.5 1 28 5l-4.5 4' stroke='black' stroke-width='1' fill='none'/%3E%3C/svg%3E") center / contain no-repeat;
    flex: none;
  }
  @media (max-width: 900px) {
    .month-card, .month-card--flip { grid-template-columns: 1fr; gap: var(--sp-6); }
    .month-media, .month-card--flip .month-media { order: 0; }
    .month-media { aspect-ratio: 3 / 2; }
    .month-copy { padding: 0; }
  }

  /* Services — the Who-We-Work-With signpost, mirrored: the list takes the
     wide LEFT column, the intro and its dark booking box the narrow right.
     Every row is an accordion — 2px rules, an accent rule that fills across
     the open row, a washed panel underneath. */
  .services-split {
    display: grid;
    grid-template-columns: minmax(0, 66fr) minmax(0, 34fr);
    gap: 0 clamp(var(--gap-grid), 5vw, 96px);
    align-items: start;
  }
  /* DOM order stays intro-then-list (reading order); the grid mirrors it */
  .services-head { grid-column: 2; grid-row: 1; max-width: 42ch; }
  .services-head .eyebrow { margin-bottom: var(--space-eyebrow); }
  .services-head h2 { font-size: clamp(2.125rem, 3.2vw, 3.25rem); }
  .services-head p { margin-top: var(--space-stack); color: var(--muted); font-size: var(--fs-lede); line-height: 1.4; }
  .services-grid { grid-column: 1; grid-row: 1; }

  /* the dark booking box — this section's call path */
  .callbox {
    margin-top: var(--sp-10);
    background: var(--accent-ink);
    padding: clamp(28px, 2.4vw, 40px);
  }
  .callbox .eyebrow { color: var(--label-ondark); margin-bottom: var(--sp-5); }
  .callbox h3 {
    font-family: var(--serif);
    font-weight: var(--fw-display);
    font-size: clamp(1.5rem, 1.9vw, 2rem);
    line-height: 1.14;
    letter-spacing: var(--tracking-display);
    color: var(--white);
  }
  .callbox p { margin-top: var(--sp-4); color: var(--ink-ondark); font-size: var(--fs-base); line-height: 1.5; }
  .callbox .btn { margin-top: var(--space-cta); }

  .svc-list { list-style: none; margin-inline: calc(var(--sp-5) * -1); border-bottom: var(--rule-strong) solid var(--rule); }
  .svc { position: relative; border-top: var(--rule-strong) solid var(--rule); padding-inline: var(--sp-5); }
  /* the accent rule fills across the row's top edge on hover/highlight
     (scaled, not width-animated, so the fill never touches layout) */
  .svc::after {
    content: '';
    position: absolute;
    top: -2px; left: 0;
    width: 100%;
    height: 2px;
    background: var(--accent);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 0.5s var(--ease-out);
    pointer-events: none;
    z-index: 1;
  }
  .svc:hover::after, .svc.is-active::after { transform: scaleX(1); }
  /* the whole row washes — the same hover the Who-We-Work-With rows use */
  .svc:hover, .svc.is-active { background: var(--wash); }
  .svc-head {
    width: 100%;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 30px;
    gap: 0 var(--sp-10);
    align-items: start;
    padding: var(--sp-9) 0;
    background: none;
    border: none;
    cursor: pointer;
    font: inherit;
    color: inherit;
    text-decoration: none;
    text-align: left;
    transition: background-color var(--dur-base) ease;
  }
  .svc-name {
    display: block;
    font-family: var(--serif);
    font-weight: var(--fw-display);
    font-size: clamp(1.5rem, 2.1vw, 2.4rem);
    line-height: 1.14;
    letter-spacing: var(--tracking-display);
    color: var(--accent-ink);
  }
  .svc-desc { display: block; margin-top: 14px; font-size: var(--fs-base); color: var(--muted); max-width: 52ch; text-wrap: pretty; }
  /* the arrow: one hairline shaft and an open head, nudged on hover. It
     points at the booking form the whole row links to, so it never turns. */
  .svc-arrow {
    grid-column: 2;
    grid-row: 1;
    width: 30px; height: 10px;
    justify-self: end;
    margin-top: 0.6em;
    overflow: visible;
    transition: transform 0.34s var(--ease-out);
  }
  .svc-arrow path { stroke: var(--accent); stroke-width: 1; fill: none; }
  .svc-head:hover .svc-arrow, .svc.is-active .svc-arrow { transform: translateX(8px); }
  @media (prefers-reduced-motion: reduce) {
    .svc::after, .svc-arrow { transition: none; }
  }
  /* auto-cycle progress rule — it IS the highlighted row's bottom rule, filling left to right */
  .svc-progress {
    position: absolute;
    left: 0; right: 0; bottom: -1px;
    height: 2px;
    overflow: hidden;
    opacity: 0;
    transition: opacity 0.3s ease;
    z-index: 1;
  }
  .svc.is-active .svc-progress { opacity: 1; }
  .svc-progress i { display: block; height: 100%; width: 0; background: var(--accent); }
  .svc.is-active.cycling .svc-progress i { animation: svcProgress 10s linear forwards; }
  @keyframes svcProgress { from { width: 0; } to { width: 100%; } }
  .services-grid:hover .svc-progress i { animation-play-state: paused; }
  @media (prefers-reduced-motion: reduce) { .svc-progress { display: none; } }
  @media (max-width: 900px) {
    .services-split { grid-template-columns: 1fr; gap: var(--gap-grid-tight); }
    .services-head { grid-column: 1; grid-row: 1; max-width: none; }
    .services-grid { grid-column: 1; grid-row: 2; }
    .svc-list { margin-inline: calc(var(--sp-4) * -1); }
    .svc { padding-inline: var(--sp-4); }
    .svc-head { gap: 10px var(--sp-5); padding: var(--sp-7) 0; }
  }

  /* Campaign strip — full-bleed continuous marquee at the hero's foot.
     The outer track clips the long marquee; vertical padding gives the
     hover-pop room to breathe inside the clip box so it isn't cut off */
  @keyframes emailMarquee {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }
  }
  /* clip the marquee horizontally only — hover scale + shadow need vertical room */
  .strip { position: relative; z-index: 2; padding: 0 0 14px; overflow: hidden; overflow-x: clip; overflow-y: visible; background: var(--bg); }
  .email-track { overflow: hidden; overflow-x: clip; overflow-y: visible; padding: 42px 0; }
  .email-marquee {
    display: flex;
    align-items: center;
    gap: 22px;
    width: max-content;
    padding: 0 11px;
    animation: emailMarquee 60s linear infinite;
  }
  .email-marquee:hover { animation-play-state: paused; }
  .email-card {
    flex: 0 0 auto;
    width: 308px;
    height: 428px;
    border: 1px solid var(--line);
    /* rounded on purpose — the one break from the square language. See the
       --r-email note in tokens.css: the card is standing in for a phone. */
    border-radius: var(--r-email);
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
    background: var(--bg-card);
    box-shadow: var(--shadow);
    cursor: default;
    transform-origin: center;
    transition: transform 0.4s var(--ease-out), box-shadow 0.4s ease;
  }
  .email-card::-webkit-scrollbar { display: none; }
  .email-card img {
    width: 100%;
    height: auto;
    display: block;
  }
  .email-card:hover {
    transform: scale(1.1);
    z-index: 5;
    box-shadow: var(--elev-5);
  }
  @media (prefers-reduced-motion: reduce) {
    .email-marquee { animation: none; }
    .email-card { transition: none; }
    .email-card:hover { transform: none; }
  }

  /* Off-screen sections keep their CSS animations queued, not running.
     A shared IntersectionObserver toggles this class per section. */
  .anim-pause,
  .anim-pause::before,
  .anim-pause::after,
  .anim-pause *,
  .anim-pause *::before,
  .anim-pause *::after { animation-play-state: paused !important; }

  /* Tools */
  .tools { background: var(--bg-soft); position: relative; overflow: hidden; }
  .tools .container { position: relative; z-index: 1; }
  /* The copy runs right-justified against the container's right edge.
     .calendly-card is a SIBLING of .tools-grid, not a child, so only the h2
     and its two paragraphs move — the booking widget stays centred beneath.
     That asymmetry is the composition, not an oversight. margin-right must be
     0 (auto on both sides would re-centre the 940px measure). */
  /* Centred, deliberately. This section was right-justified for a day and
     Nicholas reverted it: the Calendly card is a sibling of .tools-grid, not
     a child, so the copy moved right while the widget stayed centred and the
     two stopped agreeing with each other. The float-icon field is built round
     a centred axis too (see the gutter offsets below). */
  .tools-grid {
    max-width: 940px;
    margin: 0 auto;
    text-align: center;
  }
  /* A&O's ContentColumnCard pill: a 48x4 accent block opening the column.
     Auto on BOTH margins centres it over the heading; the bottom margin grows
     1.5rem -> 3rem across breakpoints, as theirs does. */
  .tools-copy::before {
    content: '';
    display: block;
    width: 48px;
    height: 4px;
    background: var(--accent);
    margin: 0 auto clamp(24px, 3vw, 48px);
  }
  .tools-copy p { margin-top: 22px; color: var(--muted); font-size: var(--fs-lede); line-height: 1.4; }

  /* floating brand logos — a depth field: size, opacity and drift speed move
     together (big = far = faint = slow). Tune one line per icon below. */
  .float-icon {
    position: absolute;
    width: var(--sz, 46px);
    height: auto;            /* the PNG icons carry width/height attrs; without this
                                the attribute's raw pixel height wins and they stretch */
    opacity: var(--op, 0.16);
    pointer-events: none;
    z-index: 0;
    animation: var(--fk, floatBob) var(--dur, 9s) ease-in-out var(--del, 0s) infinite alternate;
  }
  @keyframes floatBob   { from { transform: translateY(-11px); } to { transform: translateY(13px); } }
  @keyframes floatDrift { from { transform: translate(-9px, -8px) rotate(-2deg); } to { transform: translate(10px, 9px) rotate(2deg); } }
  @keyframes floatSway  { from { transform: translateX(-10px) rotate(2.5deg); } to { transform: translateX(9px) rotate(-2.5deg); } }

  /* left gutter (calendly card owns the centre ~900px — stay outside it) */
  .float-icon--1  { top: 8%;  left: 4%;   --sz: 92px; --op: 0.08; --dur: 14s; --fk: floatDrift; }
  .float-icon--2  { top: 30%; left: 8%;   --sz: 68px; --op: 0.14; --dur: 11s; --fk: floatBob;   --del: -4s; }
  .float-icon--3  { top: 52%; left: 3%;   --sz: 46px; --op: 0.20; --dur: 9s;  --fk: floatSway;  --del: -2s; }
  .float-icon--4  { top: 72%; left: 7.5%; --sz: 46px; --op: 0.20; --dur: 8.5s; --fk: floatDrift; --del: -6s; }
  .float-icon--5  { top: 88%; left: 3.5%; --sz: 32px; --op: 0.26; --dur: 7s;  --fk: floatBob;   --del: -1s; }
  .float-icon--6  { top: 18%; left: 1.5%; --sz: 24px; --op: 0.32; --dur: 6s;  --fk: floatSway;  --del: -3s; }
  .float-icon--7  { top: 44%; left: 9%;   --sz: 24px; --op: 0.32; --dur: 6.5s; --fk: floatBob;  --del: -5s; }
  /* right gutter — starts at 41%, not 10%. The copy is right-justified now,
     so it owns the top-right corner down to ~34% of the section; only the
     stretch beside the Calendly card is free on this side. There is no room
     for a bottom band at desktop: .tools.seamed sets padding-bottom to 48px
     here, and the 90px band the ≤900 block relies on doesn't exist until
     that breakpoint. Nothing goes past right:8% either — below ~1280 the
     900px-wide card eats the gutter from the inside. */
  .float-icon--8  { top: 10%; right: 3%;  --sz: 92px; --op: 0.08; --dur: 13s; --fk: floatBob;   --del: -7s; }
  .float-icon--13 { top: 22%; right: 1.5%; --sz: 24px; --op: 0.32; --dur: 6s; --fk: floatDrift; --del: -0.8s; }
  .float-icon--9  { top: 34%; right: 7%;  --sz: 68px; --op: 0.14; --dur: 10.5s; --fk: floatSway; --del: -2.5s; }
  .float-icon--10 { top: 68%; right: 2.5%; --sz: 46px; --op: 0.20; --dur: 9.5s; --fk: floatDrift; --del: -8s; }
  .float-icon--11 { top: 80%; right: 6%;  --sz: 32px; --op: 0.26; --dur: 7.5s; --fk: floatBob;  --del: -1.5s; }
  .float-icon--12 { top: 91%; right: 3.5%; --sz: 32px; --op: 0.26; --dur: 7s;  --fk: floatSway;  --del: -4.5s; }
  @media (max-width: 900px) {
    /* no side gutters left — redistribute the field into a top band (between
       seam and heading) and a bottom band (the tools section's 120px bottom
       padding). px offsets: % tracks section height and drifts into the
       widget. Bob/drift travel is ~13px, so keep 14px+ clearance. */
    .float-icon--3, .float-icon--4, .float-icon--6, .float-icon--7, .float-icon--13 { display: none; }
    /* top band */
    .float-icon--1  { top: 58px;  left: 6%;  --sz: 64px; --op: 0.12; }
    .float-icon--8  { top: 66px;  right: 7%; --sz: 56px; --op: 0.12; }
    .float-icon--2  { top: 116px; left: 44%; --sz: 40px; --op: 0.18; }
    /* bottom band */
    .float-icon--5  { top: auto; bottom: 52px; left: 8%;   --sz: 44px; --op: 0.24; }
    .float-icon--9  { top: auto; bottom: 22px; left: 30%;  --sz: 56px; --op: 0.16; }
    .float-icon--10 { top: auto; bottom: 56px; left: 52%;  --sz: 52px; --op: 0.20; }
    .float-icon--11 { top: auto; bottom: 18px; right: 22%; --sz: 42px; --op: 0.24; }
    .float-icon--12 { top: auto; bottom: 54px; right: 6%;  --sz: 44px; --op: 0.24; }
  }
  @media (prefers-reduced-motion: reduce) { .float-icon { animation: none; } }


  /* Curved section seams — the coloured bands don't meet the page on a
     straight line. The pseudo paints the NEIGHBOUR's colour, masked to the
     lens curve: top edges dish down toward the centre, bottom edges arch down.
     The footer is deliberately NOT seamed — it meets the page square. */
  .seamed { position: relative; }
  .seamed > .container, .seamed > .t-stage { position: relative; z-index: 2; }
  .seamed::before {
    content: '';
    position: absolute;
    top: -1px; left: 0; right: 0;
    height: var(--seam-t, 0px);
    background: var(--seam-tc, var(--bg));
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0h100v2C66 11.5 34 11.5 0 2Z'/%3E%3C/svg%3E") 0 0 / 100% 100% no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0h100v2C66 11.5 34 11.5 0 2Z'/%3E%3C/svg%3E") 0 0 / 100% 100% no-repeat;
    pointer-events: none;
    z-index: 1;
  }
  .seamed::after {
    content: '';
    position: absolute;
    bottom: -1px; left: 0; right: 0;
    height: var(--seam-b, 0px);
    background: var(--seam-bc, var(--bg));
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12v-8C34 11.5 66 11.5 100 4v8Z'/%3E%3C/svg%3E") 0 100% / 100% 100% no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12v-8C34 11.5 66 11.5 100 4v8Z'/%3E%3C/svg%3E") 0 100% / 100% 100% no-repeat;
    pointer-events: none;
    z-index: 1;
  }
  .problem.seamed { --seam-t: 48px; --seam-b: 48px; }
  /* the azure band: paper above (default --seam-tc), navy below — the navy
     band paints its own top seam, so this one needs no bottom curve */
  /* the navy band sits under the azure one, so its top seam paints AZURE,
     and its bottom seam paints the paper of #about below it */
  .testimonials.seamed {
    --seam-t: 110px; --seam-tc: var(--bg);
    --seam-b: 110px; --seam-bc: var(--bg);
    /* Both seams are 110px and the bottom one arches UP into the content, so
       the old 90/56 put the cards closest to the edge exactly where the curve
       intrudes most. That asymmetry was the claustrophobia, not the height. */
    padding-top: 110px; padding-bottom: var(--sp-24);
  }
  .tools.seamed { --seam-t: 110px; padding-top: 46px; padding-bottom: var(--sp-12); }

  /* shallower seams + tighter band padding at real scale — the stretched
     mask (preserveAspectRatio none) turns 110px into a gouge on phones */
  @media (max-width: 900px) {
    .problem.seamed { --seam-t: 28px; --seam-b: 28px; }
    .testimonials.seamed { --seam-t: 54px; --seam-b: 54px; padding-top: 52px; padding-bottom: var(--sp-9); }
    .tools.seamed { --seam-t: 54px; padding-top: 30px; }
  }
  @media (max-width: 640px) {
    .problem.seamed { --seam-t: 16px; --seam-b: 16px; }
    .testimonials.seamed { --seam-t: 30px; --seam-b: 30px; padding-bottom: var(--sp-7); }
    .tools.seamed { --seam-t: 30px; }
  }

  /* calm-lake breathing — the seam curve swells a few px on a slow tide.
     Mask height eases 100%->93%; the flat edge stays glued to its neighbour
     (top seams anchor top, bottom seams anchor bottom), so only the curve moves. */
  @keyframes seamBreathe {
    from { -webkit-mask-size: 100% 100%; mask-size: 100% 100%; }
    to   { -webkit-mask-size: 100% 88%;  mask-size: 100% 88%; }
  }
  .seamed::before { animation: seamBreathe 12s ease-in-out infinite alternate; }
  .seamed::after  { animation: seamBreathe 15s ease-in-out -7s infinite alternate; }
  @media (prefers-reduced-motion: reduce) {
    .seamed::before, .seamed::after { animation: none; }
  }


  /* FAQ — an editorial plate on the left running off the viewport, the
     accordion on the right. Same bleed expression as .month-media in
     #process, so the two sections' outer edges land on the same line, and
     #faq clips X for the same reason #process does. */
  #faq { overflow-x: clip; }
  .faq-wrap {
    display: grid;
    grid-template-columns: minmax(0, 46fr) minmax(0, 54fr);
    gap: 0 clamp(var(--gap-grid), 4vw, 72px);
    align-items: center;
    max-width: none;
    margin: 0;
  }
  /* the accordion keeps its own 760px measure; only the centring is dropped,
     so it sits flush right under the right-justified header. width:100% is
     load-bearing: an auto margin on a grid item cancels justify-self:stretch,
     and the list would shrink-to-fit to ~512px without it. */
  .faq-wrap .faq-list { width: 100%; margin-left: auto; margin-right: 0; }
  .faq-media { margin-left: calc(-1 * (var(--gutter, 48px) + max(0px, (100vw - 1856px) / 2))); }
  /* The plate, copied from .month-media: 3:2, soft ground while the photo
     decodes. The clip lives on <picture>, NOT on .faq-media — same split as
     #process, so anything that later has to straddle the plate's inner edge
     (a pop band, a numeral) sits on .faq-media, outside the clip. Here the
     clip earns its keep twice over: it holds the wipe slab AND the image's
     scale(1.5) start frame. */
  .faq-media { position: relative; aspect-ratio: 3 / 2; background: var(--bg-soft); }
  .faq-media picture { display: block; position: relative; width: 100%; height: 100%; overflow: hidden; }
  .faq-media img { width: 100%; height: 100%; object-fit: cover; display: block; }

  /* The pink wipe — A&O's MediaWipe without the library. A slab twice the
     frame's width sweeps across and off while the photo rises from
     scale(1.5) behind it. The slab is <picture>'s ::after, not a <span>:
     <picture>'s content model admits only <source>/<img>, and the slab has
     to be INSIDE the clip or it hangs over the accordion at rest.
     Direction is reversed from A&O's: theirs sweeps left-to-right, ours
     exits LEFT, following the plate's own bleed, so the reveal edge starts
     at the accordion side and travels out of frame with the photo.
     These are animations, never transitions, on purpose: .anim-pause holds
     animation-play-state while the section is off-screen and does nothing at
     all to a transition, so a transition here would burn its whole run
     un-watched the moment the 3s reveal fallback fires. */
  .faq-media picture::after {
    content: '';
    position: absolute;
    inset: 0 auto 0 0;
    z-index: 1;                 /* over the photo; nothing else is in the clip */
    width: 200%;
    height: 100%;
    background: var(--pop-pink);
    transform: translateX(-100%);   /* rest / no-JS: cleared, parked off-left */
    pointer-events: none;
  }
  /* The reveal observer unobserves on first intersection, so .in is added
     once and never removed — the wipe is one-shot without any extra state. */
  /* 50%, not A&O's 100%: at translateX(50%) the 200%-wide slab's left edge is
     flush with the frame's right edge, so the run has no dead lead-in. From
     50% to -100% it is over the whole plate for the middle third of the run —
     that dwell is what the photo rises inside. */
  .js .reveal    .faq-media picture::after { transform: translateX(50%); }
  /* --ease-standard, not --ease-out, for the slab alone: the slab travels
     three frame-widths, and under --ease-out (0.2,0.8,0.2,1) it is 83% done by
     0.3s — the crossing blurs past and the last sliver crawls. The photo keeps
     --ease-out, which is right for an opacity/scale settle. */
  .js .reveal.in .faq-media picture::after { animation: fq-wipe 1s var(--ease-standard) 0.15s forwards; }
  @keyframes fq-wipe { from { transform: translateX(50%); } to { transform: translateX(-100%); } }
  .js .reveal    .faq-media img { opacity: 0; }
  .js .reveal.in .faq-media img { animation: fq-rise 1s var(--ease-out) 0.15s forwards; }
  /* the 0%,34% hold is the choreography, not padding: the photo must not be
     visible BEFORE the slab reaches it, or the plate reads photo-pink-photo.
     The hold ends inside the covered dwell, so the slab uncovers a photo that
     has already arrived — one duration and one delay for both halves.
     fq-rise used to open at scale(1.5) and settle to none, i.e. it zoomed OUT.
     The scale came off it on 28 Aug because the scroll timeline below now owns
     transform on this img and two animations cannot both hold one property —
     the later one in the list simply wins and the other silently does nothing.
     fq-rise is the fade alone now; the zoom direction is the other way. */
  @keyframes fq-rise {
    0%, 34% { opacity: 0; }
    100%    { opacity: 1; }
  }
  /* Scroll-driven zoom: the photo grows as the plate travels up the viewport
     and then STOPS and holds — asked for on 28 Aug, and the stop is the point,
     not a side effect. `forwards` is what holds it: the range closes at
     cover 40%, well before the plate leaves, so the last two-thirds of the
     plate's travel show a settled image rather than one still creeping.
     A scroll timeline, not a scroll listener, and the reason is the same one
     that cost a whole task on the iMessage thread: .anim-pause freezes
     animation-play-state while a section is off-screen and cannot touch a
     transition or a JS handler, so mixing mechanisms desyncs. It cannot desync
     here — a view() timeline is driven by position, not elapsed time, so a
     pause holds progress and un-pausing snaps straight back to where the
     scroll actually is.
     1.12, not the 1.5 fq-rise used to open at. The frame is 3:2 and the source
     2:3, so cover already keeps only the middle ~44% of the source height, and
     object-position 50% 0% has that band pinned to the top where the ~6% of
     headroom above her hair is. Scaling about the frame's centre pulls the
     band's top edge DOWN: at 1.12 it lands at ~2.4% of the source, still above
     her crown, and the crown is only reached around 1.37. The ceiling that
     actually bites is resolution, not framing — the master is 1200px wide and
     the desktop plate is ~1090, so 1.12 is already sampling past 1:1.
     Two animations on one element, so every part is a LIST and the order has
     to match: fq-rise gets the document timeline (none) and its own
     duration/delay; fq-zoom gets view(). Writing `animation:` as a shorthand
     after this would reset animation-timeline to auto and kill the zoom, which
     is why every part is spelled out as a longhand here.
     animation-duration MUST be `auto` on the scroll-timeline half. A time
     duration on a progress-based timeline is not ignored — it is honoured, and
     it burns the whole 1s inside the first frame of the range, so the photo
     arrives already fully zoomed and there is no ramp at all to watch. That is
     exactly what the first attempt did. `auto` is what maps the keyframes onto
     the animation-range instead of onto a clock. */
  @supports (animation-timeline: view()) {
    /* the timeline is NAMED and declared here, on .faq-media, and that is the
       whole trick. An anonymous view() on the <img> silently does not work:
       view() takes its scroll container from the nearest ancestor that IS one,
       <picture> is overflow:hidden, and overflow:hidden makes a scroll
       container even though it never scrolls. The img's timeline resolved to
       that dead scroller, so it sat pinned at 100% — fully zoomed, at every
       scroll position, with no ramp and no error anywhere. .faq-media is
       OUTSIDE the clip, so its own scroller is the page. */
    .faq-media { view-timeline-name: --faq-plate; }
    .faq-media img {
      animation-name: fq-zoom;
      animation-duration: auto;
      animation-timing-function: linear;
      animation-fill-mode: forwards;
      animation-timeline: --faq-plate;
      animation-range: entry 0% cover 40%;
    }
    /* `auto` is the document timeline. `none` is NOT — none detaches the
       animation entirely, and fq-rise then never runs, which leaves the photo
       at opacity 0 for good behind a wipe that plays perfectly. */
    .js .reveal.in .faq-media img {
      animation-name: fq-rise, fq-zoom;
      animation-duration: 1s, auto;
      animation-timing-function: var(--ease-out), linear;
      animation-delay: 0.15s, 0s;
      animation-fill-mode: forwards, forwards;
      animation-timeline: auto, --faq-plate;
      animation-range: normal, entry 0% cover 40%;
    }
  }
  @keyframes fq-zoom {
    from { transform: scale(1); }
    to   { transform: scale(1.12); }
  }
  /* The base .reveal reduce rule can't reach these: park the slab off-frame
     and show the photo outright. */
  @media (prefers-reduced-motion: reduce) {
    .js .reveal .faq-media picture::after,
    .js .reveal.in .faq-media picture::after { animation: none; transform: translateX(-100%); }
    /* the bare .faq-media img is in this list for the scroll zoom, which is
       the one animation here that also runs without JS. `animation: none` is
       the shorthand on purpose — it resets animation-timeline back to auto,
       so it takes the view() timeline off with it. */
    .faq-media img,
    .js .reveal .faq-media img,
    .js .reveal.in .faq-media img { animation: none; opacity: 1; transform: none; }
  }
  @media (max-width: 900px) {
    /* one column: the plate goes full-bleed on BOTH edges above the list */
    .faq-wrap { grid-template-columns: 1fr; gap: var(--gap-grid-tight); }
    .faq-wrap .faq-list { margin-left: 0; }
    .faq-media {
      margin-left: calc(-1 * var(--gutter, 48px));
      margin-right: calc(-1 * var(--gutter, 48px));
    }
  }

  /* Testimonials — one review centred, neighbours teased at the page edges,
     arrows + swipe + autoplay, looping over a tripled set. --vwz is the true
     full-bleed width in layout px (vw shrinks under the body zoom). */
  /* THE duo-tone statement band: navy ground, everything in the pale tint —
     quote, heading, hairlines — and the bright blue reserved for the two
     things that should catch: the client's name and the carousel arrows. */
  .testimonials { background: var(--accent-ink); position: relative; }
  .testimonials h2 { color: var(--ink-ondark); }
  .t-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--sp-10); max-width: none; }
  .t-nav { display: flex; gap: var(--sp-3); flex: none; padding-bottom: 6px; }
  .t-arrow {
    width: 66px; height: 48px;
    display: grid;
    place-items: center;
    background: none;
    border: var(--rule-hairline) solid color-mix(in srgb, var(--ink-ondark) 34%, transparent);
    border-radius: var(--radius-card);
    cursor: pointer;
    transition: background-color var(--dur-base) ease, border-color var(--dur-base) ease;
  }
  .t-arrow svg { width: 30px; height: 10px; overflow: visible; display: block; }
  .t-arrow path { stroke: var(--accent-ondark); stroke-width: 1; fill: none; }
  .t-arrow--prev svg { transform: scaleX(-1); }
  .t-arrow:hover, .t-arrow:focus-visible { background: var(--chip-ondark); border-color: var(--accent-ondark); }
  /* same fault, same fix as .btn--ondark: the global ring is 2px of --accent,
     which is 1.50:1 on this band's --accent-ink ground — a keyboard ring
     nobody can see. --accent-ondark is 9.60:1 there. The ring is painted
     outside the border box, so the ground under it is the band's navy, not the
     arrow's own hover fill; the global 3px offset keeps a clear navy gap
     between the ring and the arrow's --accent-ondark border, and 2px of ring
     plus 3px of offset each side still clears the 12px .t-nav gap, so two
     focused arrows can never share a ring. */
  .t-arrow:focus-visible { outline-color: var(--accent-ondark); }
  /* the rail is a scroll container, so Chrome puts it in the tab order on its
     own — no href, no tabindex, invisible to a selector sweep, and it lands on
     the same navy. Found by tabbing the page, not by grepping it. */
  .testimonial-rail:focus-visible { outline-color: var(--accent-ondark); }
  @media (prefers-reduced-motion: reduce) { .t-arrow { transition: none; } }
  @media (max-width: 640px) {
    .t-head { flex-direction: column; align-items: flex-start; gap: var(--sp-6-5); }
  }
  .testimonials { --vwz: calc(100vw / var(--page-scale)); --tcard: min(680px, calc(var(--vwz) - 120px)); }
  .t-stage { position: relative; }
  .testimonial-rail {
    display: flex;
    gap: var(--sp-6);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    padding: 6px calc((var(--vwz) - var(--tcard)) / 2) 22px;
  }
  .testimonial-rail::-webkit-scrollbar { display: none; }
  .t-stage { cursor: pointer; } /* click either half of the stage to step */
  .testimonial {
    flex: 0 0 var(--tcard);
    transition: opacity 0.4s ease, transform 0.4s var(--ease-out);
    display: flex;
    flex-direction: column;
    scroll-snap-align: center;
    background: none;
    border: var(--rule-hairline) solid color-mix(in srgb, var(--ink-ondark) 32%, transparent);
    border-radius: var(--radius);
    padding: 38px var(--sp-10);
  }
  /* the quote is the one thing on this band that must be readable at length,
     so it takes the light tint (8.75:1) rather than the bright blue */
  .testimonial p {
    font-size: var(--fs-md);
    line-height: 1.7;
    color: var(--ink-ondark);
    margin-bottom: var(--sp-6-5);
  }
  .testimonial .author { font-weight: var(--fw-semibold); color: var(--accent-ondark); }
  .testimonial .role { color: var(--muted-ondark); font-size: var(--fs-base); }
  /* the underlined-field hairline from the reference: one pale rule between
     the quote and whose it is */
  .testimonial-who {
    display: flex;
    align-items: center;
    gap: var(--sp-4);
    margin-top: auto;
    padding-top: var(--sp-6-5);
    border-top: var(--rule-hairline) solid color-mix(in srgb, var(--ink-ondark) 24%, transparent);
  }
  .testimonial-logo {
    width: 54px; height: 54px;
    object-fit: contain;
    border-radius: var(--r-md);
    background: var(--white);   /* client marks need their own white plate */
    border: 0;
    padding: 6px;
  }
  .testimonial:not(.is-center) { opacity: 0.55; transform: scale(0.97); }
  @media (max-width: 640px) {
    .testimonials { --tcard: calc(var(--vwz) - 72px); }
    .testimonial { padding: 30px var(--sp-6-5); }

  }

  /* About — the split: copy left, a large black-and-white portrait bleeding
     off the right edge, and the site's ONE hot-pink moment overlapping it. */
  #about { overflow-x: clip; }
  .about-grid {
    display: grid;
    grid-template-columns: minmax(0, 38fr) minmax(0, 62fr);
    gap: 0 clamp(40px, 5vw, 88px);
    align-items: center;
    max-width: none;
    margin: 0;
  }
  .about-copy h2 { font-size: clamp(2.125rem, 3.6vw, 3.25rem); line-height: 1.14; }
  .about-photo {
    order: 2;
    position: relative;
    justify-self: end;
    width: 100%;
    /* the source is 600px wide, so it softens past ~760 — owner-approved
       trade: the portrait fills its column instead. The plate now tracks the
       source's own proportion, so no dead ground sits above his head. */
    max-width: 1040px;
    height: auto;
    aspect-ratio: 600 / 582;
    border-radius: var(--radius-card);
    border: 0;
    box-shadow: none;
    background: var(--bg-soft);
    overflow: visible;
  }
  .about-photo picture { display: block; width: 100%; height: 100%; }
  .about-photo img {
    width: 100%; height: 100%;
    object-fit: contain;
    object-position: bottom center;
    display: block;
    filter: grayscale(1) contrast(1.05);
  }
  /* A pink mark — one of three uses of --pop-pink, not "the only" one as this
     comment used to claim. See the count of record in assets/tokens.css. */
  .about-photo::after {
    content: '';
    position: absolute;
    left: clamp(-30px, -2vw, -20px);
    bottom: clamp(-30px, -2vw, -20px);
    width: clamp(76px, 8vw, 124px);
    height: clamp(76px, 8vw, 124px);
    background: var(--pop-pink);
    z-index: 2;
  }
  .about-copy p { margin-top: 22px; color: var(--muted); font-size: var(--fs-lede); line-height: 1.4; max-width: 44ch; }

  /* Fit */
  .fit-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--sp-7);
  }
  .fit-col {
    background: var(--bg-card);
    border: var(--rule-hairline) solid var(--line);
    border-radius: var(--radius);
    padding: 38px 38px 30px;
    box-shadow: var(--shadow);
  }
  .fit-col h3 { margin-bottom: 18px; }
  .fit-col ul { list-style: none; }
  .fit-col li {
    position: relative;
    padding: 11px 0 11px 26px;
    color: var(--muted);
    font-size: var(--fs-base);
    border-top: var(--rule-hairline) solid var(--line);
  }
  .fit-col li:first-child { border-top: none; }
  .fit-col li::before {
    content: '';
    position: absolute;
    /* top sits the dot on the first line's optical centre (line box centres at
       25px inside the 11px-padded row; 21 puts the dot a touch above, on the
       x-height side, which is where a round marker wants to be) */
    left: 2px; top: 21px;
    width: 6px; height: 6px;
    border-radius: 50%;
    background: var(--accent);
  }

  /* FAQ */
  .faq-list { max-width: 760px; margin: 0 auto; }
  /* hairline-ruled questions, same list grammar as the services rows */
  .faq-list details {
    position: relative;
    border-top: var(--rule-strong) solid var(--rule);
    background: none;
    transition: background 0.3s ease;
  }
  /* the accent rule fills across the row's top edge on hover/open */
  .faq-list details::after {
    content: '';
    position: absolute;
    top: -2px; left: 0;
    width: 100%;
    max-width: 0;
    height: 2px;
    background: var(--accent);
    transition: max-width 0.5s var(--ease-out);
    pointer-events: none;
  }
  /* A&O's accordion rule: hovering a row also fills the NEXT row's bar, so the
     divider between the two lights end to end instead of only its top half. */
  .faq-list details:hover::after,
  .faq-list details:hover + details::after,
  .faq-list details[open]::after { max-width: 100%; }
  .faq-list details[open] { background: var(--wash); }
  @media (prefers-reduced-motion: reduce) {
    .faq-list details::after { transition: none; }
  }
  .faq-list details:last-of-type { border-bottom: var(--rule-accent) solid var(--accent); }
  .faq-list summary {
    cursor: pointer;
    padding: var(--sp-6) var(--sp-1);
    font-weight: var(--fw-medium);
    font-size: var(--fs-md);
    color: var(--accent-ink);
    list-style: none;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--sp-6);
    transition: color var(--dur-fast) ease;
  }
  .faq-list summary:hover { color: var(--accent-text); }
  .faq-list summary::-webkit-details-marker { display: none; }
  /* thin 1px chevron, drawn as a mask so the stroke stays hairline */
  .faq-list summary::after {
    content: '';
    flex-shrink: 0;
    width: 14px; height: 14px;
    background: var(--accent);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M1.5 4.5 7 10l5.5-5.5' stroke='black' stroke-width='1.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M1.5 4.5 7 10l5.5-5.5' stroke='black' stroke-width='1.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
    transition: transform var(--dur-fast) var(--ease-out);
  }
  .faq-list details[open] summary::after { transform: rotate(180deg); }
  .faq-list details p {
    padding: 0 var(--sp-16) var(--sp-6-5) var(--sp-1);
    color: var(--muted);
    font-size: var(--fs-base);
    max-width: 68ch;
  }

  /* Booking embed (lives in the Highest-Leverage section) */
  .calendly-card {
    max-width: 900px;
    margin: var(--sp-6) auto 0;
  }
  .calendly-card .cal-embed { min-width: 0; height: 620px; } /* tight: Calendly centres its panel vertically, slack reads as dead padding; page_height grows it if needed */
  @media (max-width: 900px) {
    .calendly-card { margin-top: var(--sp-7); }
    .calendly-card .cal-embed { height: 800px; } /* no internal scroll; JS trims to exact height */
    .tools.seamed { padding-bottom: 90px; } /* bottom band for the float icons */
  }
  @media (max-width: 640px) {
    .calendly-card { margin-left: calc(var(--sp-3) * -1); margin-right: calc(var(--sp-3) * -1); } /* slight inset, not hard full-bleed */
    .calendly-card .cal-embed { height: 850px; } /* calendar view fits; page_height grows it for taller steps */
  }

  /* Responsive */
  @media (max-width: 900px) {
    .section--tight { padding-top: var(--sp-6); }
    .seamed::before, .seamed::after { animation: none; }
    /* let the hero grow so the whole letter reads; desktop keeps the fold */
    .hero { height: auto; min-height: 0; padding-bottom: var(--sp-10); }
    /* tablet type — desktop clamps were tuned for the 0.8 zoom */
    h2:not(.hl-host) { font-size: clamp(1.75rem, 5vw, 2.125rem); }
    .pc-center h2 { font-size: clamp(1.7rem, 3.4vw, 2.2rem); }
    .month-title { font-size: clamp(1.45rem, 3vw, 1.85rem); }
    .hero { padding: calc((var(--nav-h) + var(--nav-gap)) * 1.15 + 18px) 0 var(--sp-11); }
    /* the mock shares the nav card's 12px gutters — one aligned column */
    .hero .container { padding: 0 var(--sp-3); }
    .bento { grid-template-columns: repeat(2, 1fr); --bento-cols: 2; }
    .fit-grid { grid-template-columns: 1fr; }
    .about-grid { grid-template-columns: 1fr; gap: var(--sp-10); text-align: left; }
    .about-photo { order: 0; justify-self: stretch; max-width: none; }
    .footer-brand { grid-column: 1 / -1; }
    .footer-brand img { height: 44px; width: auto; }
  }

  /* Email marquee, touch widths. The nav's hamburger rules used to share this
     block; they are shared with the Opinions pages and now live in site.css §14
     at the same breakpoint. */
  @media (max-width: 900px) {
    /* touch devices: the strip is a swipe rail that auto-drifts via
       scrollLeft (JS) — a touch pauses it, momentum scroll takes over, and it
       resumes shortly after release. Clones stay for the seamless wrap. */
    .email-track {
      overflow-x: auto;
      -webkit-overflow-scrolling: touch;
      scrollbar-width: none;
    }
    .email-track::-webkit-scrollbar { display: none; }
    /* iOS: a transform marquee inside a scrollable rail composites badly
       (ghost card layers, clipped shadows) — the drift is done in JS via
       scrollLeft instead, and the shadow stays inside the rail padding */
    .email-track .email-marquee { animation: none; }
    /* transparent chrome: the screenshot IS the card face — iOS ghost-layer
       bleed has nothing white to show. No inner scroll: 18 composited scroll
       layers gone, and a vertical swipe on a card always scrolls the page. */
    .email-card { width: 260px; height: 400px; box-shadow: none; background: transparent; border: 0; overflow-y: hidden; }
    .email-card:hover { transform: none; box-shadow: none; z-index: auto; }  /* iOS sticky tap-hover must not pop the desktop shadow */
  }

  @media (max-width: 640px) {
    h2:not(.hl-host) { font-size: clamp(1.6rem, 7vw, 2.125rem); }
    .gmail-body { font-size: var(--fs-base); }
    .gmail-sig { font-size: var(--fs-xs); }
    .gmail-tools { display: none; }
    .bento { grid-template-columns: 1fr; grid-auto-rows: auto; --bento-cols: 1; }
    /* one full-width column: a 419/530 floor would be a 600px-tall card, so
       the proportion is dropped here and every card is content-height */
    .bcard { min-height: 0; }
    /* content-height cards have no free space to hand out, so a flex: 1 visual
       collapses to its own minimum — pin the two that rely on the fill */
    .viz-tz { flex: none; height: 260px; }
    /* the list is taller than the pinned box here — crop the tail, not the head */
    .viz-actions { flex: none; height: 240px; justify-content: flex-start; }

    /* phone-size cards for the swipe rail (mechanics live in the 900px block) */
    .email-track { padding: 24px 0; }
    .email-marquee { gap: 16px; padding: 0 14vw; } /* first/last card can centre-snap */
    .email-card { width: 72vw; height: min(115vw, 520px); }
  }
