/* ==========================================================================
   WEBSITES PAGE — /products/websites/
   Built 2026-08-28 from Alie's "Websites Page — Final Copy" (delivered 8/19).

   THIS SHEET ONLY ADDS. Everything shared comes from direction-d-v3.css +
   variant-d-v3.css, exactly as seo-content.css does:
     .container .section .section--white .section--indigo .btn .arch
     .hdr* .footer* .faq* .collapse .final-cta* .reveal [data-hero-item]
     all t-*, and the hero grid (.hero .hero__grid .hero__copy
     .hero__headline .hero__sub .hero__discs .hero__pattern).

   🔴 DELIBERATELY NOT REUSED — each would have broken something silently:

     .proof        — is position:sticky AND is queried by initProofPin() in
                     direction-d-v3.js, which THIS PAGE LOADS. The proof
                     section here is .wproof.
     .how__*       — queried by track-v3.js to drive the Call Insights scroll
                     line. This page does not load that script, so the classes
                     would do nothing now and something unintended later.
                     The steps here are .wstep*.
     .v3-seq /
     .seo-seq      — each is revealed ONLY by its own page's track script, so
                     either one here would be opacity:0 FOREVER. This page has
                     its own pair: .reveal for ordinary scroll reveals, and
                     .wsite-seq for the three steps the routing line drives.
                     See section 7.
     .section--lime— variant-d-v3.css repaints it oat, so the name lies.
     .hero__stage  — hard-codes min-height:520px for the Call Insights email
                     deck. This page's stage is .wsite-stage.
     .seo-stage /
     .costs* /
     .services*    — those live in seo-content.css, which this page does NOT
                     link. Two pages sharing one page-specific sheet is how a
                     change to one silently repaints the other.

   🔴 NO EYEBROWS ANYWHERE IN THIS SHEET. Alie, 8/19, restated as a sitewide
   rule in this page's Build Notes. The two that had crept onto the SEO page
   were removed 8/28 for the same reason. Do not add an eyebrow tier here.

   🔴 NO BOXES. Copy never goes in a bordered/filled/rounded container;
   emphasis comes from type. The one exemption the house rule names is
   hairline row separators, which is what the pricing table uses.

   CONTRAST — every value in this sheet, measured, not assumed:
     on --indigo #4c4e68 : white 8.09 · oat 7.01 · oat-75 4.77 · lime 5.44
                           petal 4.74 · sky 5.97
     on --oat    #f3eee8 : ink 11.53 · ink-muted 5.08
     on --white  #ffffff : ink 13.30 · ink-muted 5.46
   ⚠️ --ink-muted ONLY works on oat and white. It is 4.31:1 on sky and fails.
   ========================================================================== */


/* ==========================================================================
   1. HERO ADDITIONS
   ========================================================================== */

/* Two buttons side by side, wrapping to a stack on narrow screens. gap, not
   a margin on the second child, so the wrap gets vertical spacing for free. */
.wsite-hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
}

/* 🔴 .btn--ghost IS NEW AND IS DEFINED HERE, NOT IN THE SHARED SHEET.
   direction-d-v3.css ships exactly two button skins, --primary (ink fill) and
   --onDark (lime fill). The Build Notes call for a secondary/ghost style for
   "Browse Our Portfolio", so it is added on the page that needs it rather
   than pushed into the shared sheet where two other pages would inherit an
   untested variant.

   ⚠️ IT SITS ON WHITE ONLY. The hero ground is --white; --ink measures 13.30
   there, both for the label and the 1px rule. If this button is ever placed on
   --indigo or --sky it needs a re-measure and probably an --oat border. */
.btn--ghost {
  background-color: transparent;
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}
.btn--ghost:hover {
  background-color: var(--ink);
  color: var(--oat);
}

/* 🔴 THIS RULE IS NOT COSMETIC — WITHOUT IT THIS BUTTON DELETES HALF THE
   FOCUS RING, AND IT IS THE ONE BAND base-v3.css SAYS NEVER TO REMOVE.

   The focus indicator is DUAL-BAND by design: `outline: 3px var(--ink)` plus
   `box-shadow: 0 0 0 2px var(--white)`. base-v3.css records that no single hue
   clears the 3:1 indicator floor against both oat and ink, so the white band
   is what makes the ring visible on the light half of the palette — "remove
   either band and the indicator disappears on half the palette."

   .btn--ghost draws its outline with box-shadow, and :focus-visible sets
   box-shadow too. Both are single-class selectors, so specificity TIES and
   SOURCE ORDER decides — websites.css loads after base-v3.css, so the ghost
   rule would win and the white band would silently vanish on focus.

   Restating both shadows here keeps the ghost outline AND the halo. The
   outline itself is untouched and still comes from :focus-visible.
   ⚠️ Any future .btn variant that uses box-shadow needs this same pairing. */
.btn--ghost:focus-visible {
  box-shadow: inset 0 0 0 1px var(--ink), 0 0 0 2px var(--white);
}

/* "Trusted by 600+ independent practices". --ink-muted is 5.46 on white,
   comfortably over the 4.5 floor at this size. Small and quiet by weight and
   size, NOT by a lighter tier — see the sheet header. */
.wsite-trust {
  margin: var(--s-3) 0 0;
  font-size: var(--fs-small);
  color: var(--ink-muted);
}

/* ⏹ .wsite-stage DELETED 8/28 — the hero art landed. It was an empty frame
   holding space for a design that did not exist yet; the unfurling gallery is
   that design, and it is not a stage inside a grid but the whole hero. The
   rule went with the markup rather than sitting orphaned. See section 8. */


/* ==========================================================================
   2. THE PROBLEM — light blue ground
   ========================================================================== */

/* 🔴 GROUND IS --sky-soft AS OF 8/31 (Brees: "s2 - change the bg to light
   blue"). It was --oat, inherited from the default `.section`.

   ⚠️ MEASURED BEFORE THE SWAP, AND IT IS CONTRAST-NEUTRAL — which is not
   obvious, because this sheet's own colour table warns that --ink-muted
   "ONLY works on oat and white ... 4.31 on sky and fails". That warning is
   about --sky (#c6e2ee), NOT --sky-soft (#e4f1f7), which is far lighter:
       ground            ink      ink-muted
       --oat (was)      11.53      5.08
       --sky-soft       11.54      5.07
   Nothing in this section needed a colour change. Do not read the sheet's
   "fails on sky" note as covering this ground — it does not.

   ⚠️ THE SECTION CARRIES A --sky CIRCLE (.shape--sky-lg) AND IT IS NOW THE
   SAME HUE FAMILY AS ITS GROUND. Contrast is unchanged — sky measures 1.17
   on oat and 1.17 on sky-soft, identically — but it now reads as a tonal
   shape rather than a blue circle on cream. That is a LOOK change, not a
   legibility one. 📌 1.17 is under the ~1.35 shape floor either way; that
   predates this change and is not introduced by it.

   ⚠️ --sky-soft, not --sky. `.section--sky` paints full --sky, which is
   markedly more saturated; the google-ads page records the same choice. */
.wsite-problem { background-color: var(--sky-soft); }

.wsite-problem__head {
  max-width: 24ch;
  margin: 0 0 var(--s-5);
}

/* ⚠️ 24ch IS ON THE HEADING ITSELF, NOT ON A WRAPPER. A ch cap resolves
   against the font-size of the element it is DECLARED on. Put this on a
   wrapper <div> and it would resolve against inherited 16px body type instead
   of the display heading inside it — that exact mistake computed to 232px on
   the SEO page and wrapped a headline to eight lines. */

/* ⏹ THE TWO-COLUMN BODY IS GONE. It was `1fr 1fr` from 900px up. One column
   now, so the right of the section is free for the render that flies down out
   of the hero. The 62ch cap is no longer overridden at any width — it is the
   reason the paragraphs stay readable now that they have the room to run wide. */
.wsite-problem__body {
  display: grid;
  gap: var(--s-3);
  max-width: 62ch;
}

/* Copy left, landing slot right. Stacks under 900px, where the slot sits
   beneath the copy and the flight is a shorter drop. */
.wsite-problem__grid {
  display: grid;
  gap: var(--s-5);
  align-items: center;
}

@media (min-width: 900px) {
  .wsite-problem__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 26rem);
    gap: var(--s-6, var(--s-5));
  }
}

/* 🔴 `container-type` IS NOT OPTIONAL HERE — IT IS THE DOCUMENTED TRAP.
   `.wr` sizes every internal dimension in `em` off a font-size declared in
   `cqw`, and site-renders.css is explicit that the query container must be the
   tile's PARENT: "A NEW CONTEXT USING `.wr` MUST DECLARE THAT CONTAINER or the
   tiles silently blow up to viewport scale." This is a new context. Without
   this line the landed render draws roughly four times too large.
   The flier below needs it for the same reason, and gets it for the same
   reason — that is what makes the render scale smoothly during the flight
   instead of distorting: change the width and it behaves like a scaled
   screenshot. */
.wsite-problem__media {
  container-type: inline-size;
}

/* The landed render waits at zero opacity and is swapped in at the exact frame
   the flier arrives — a hard swap, never a cross-fade (see render-handoff.js).
   ⚠️ ONLY UNDER html[data-js="on"]. With JS off nothing flies, nothing swaps,
   and an unconditional rule here would leave the slot permanently blank. */
html[data-js="on"] .wsite-problem__media .wr {
  opacity: 0;
}

html[data-js="on"] .wsite-problem__media.is-landed .wr {
  opacity: 1;
}

/* The source tile keeps its space in the wall while its clone is in flight.
   🔴 `visibility`, NOT `display` OR REMOVAL. The wall's tiles are flex
   children, so taking one out of the flow would let the rest of the column
   jump up — a visible lurch in the hero at the exact moment attention is on
   the render leaving it. The earlier implementation could reparent its flier because those
   cards are absolutely positioned; these are not. */
.wr.is-handoff-source {
  visibility: hidden;
}

/* The flier itself. Fixed, so it is positioned against the viewport while the
   page scrolls under it, and reparented to <body> at take-off so the hero
   pane's `overflow: hidden` cannot clip it mid-flight.
   ⚠️ NO `height` IS SET, BY DESIGN. The tile's height follows from its width
   through the em/cqw chain, so lerping width alone scales it faithfully;
   lerping height too would fight that and squash the render. */
.wr-flier {
  position: fixed;
  /* 🔴 30, WHICH IS UNDER THE STICKY HEADER'S 40 (direction-d-v3.css:342).
     The port started at the earlier implementation's 90 and the render flew straight OVER the
     header — at take-off its top is above the viewport top, so it covered the
     lockup and two nav items for the whole first stretch of the flight.
     It still needs to outrank the section it flies across (`.container` is 2,
     the ambient shapes are 1), so it sits between the two. */
  z-index: 30;
  container-type: inline-size;
  pointer-events: none;
  /* Every frame is written imperatively — a transition here would fight the
     interpolation and lag the flier behind the scroll. */
  transition: none;
}

/* 🔴 REDUCED MOTION: THE SCRIPT RETURNS EARLY, SO THE CSS MUST FINISH THE JOB.
   Nothing flies and nothing ever adds `.is-landed`, which would leave the slot
   blank forever — the exact bug a previous build shipped and then had to fix, where
   every JS-revealed element needed a reduced-motion override. The render is
   simply present, and the hero keeps all its tiles. */
@media (prefers-reduced-motion: reduce) {
  html[data-js="on"] .wsite-problem__media .wr { opacity: 1; }
  .wr.is-handoff-source { visibility: visible; }
}


/* ==========================================================================
   3. THE APPROACH — indigo ground, three principles
   ========================================================================== */

/* THE HEADING SITS ON THE RIGHT — Brees, 8/28. The section ran title top-left
   with the three principles beneath, which left the whole right half of the
   band empty above the columns. Pushed right, the heading's right edge lines up
   with the third principle's right edge (both stop at the container), so the
   band reads as one aligned block instead of a title with a void beside it.

   ⚠️ `margin-left: auto` AND `text-align: right` TOGETHER, NOT EITHER ALONE.
   The 20ch cap means the block is much narrower than the section; moving it
   right without right-aligning leaves the text ragged AGAINST the right edge,
   which reads like a mistake rather than a decision. Both, or neither.
   📌 Only from 900px up. Stacked, the heading stays left with everything else —
   a right-aligned heading over left-aligned body copy on a phone is just
   misaligned. */
.wsite-approach__head {
  max-width: 20ch;
  margin: 0 0 var(--s-5);
  color: var(--white);
}

/* THE SECTION IS A SPLIT — toggles left, title right. Brees, 8/28, after
   trying the title right, then left with the toggles: "I'm wondering if a
   split would be good here."

   🔴 THE HEADING IS SOURCE-ORDER FIRST AND PLACED RIGHT BY THE GRID. Explicit
   `grid-column` on both children, rather than reordering the markup, so the
   reading order and the tab order still run title -> rows. A visual
   arrangement is never worth scrambling the source for.

   ⚠️ `align-self: start` AND `margin-bottom: 0`, or the title floats. Its
   stacked-layout margin is what separates it from the rows when they are below
   it; beside them it becomes a gap that pushes the title off the first row's
   top rule. Top-aligned, the heading's cap-height sits on the same line as the
   first row's hairline.

   📌 NOT STICKY, DELIBERATELY. A title that follows the rows is the obvious
   next idea, but with three rows the section is only ~600px tall — there is
   barely a viewport of scroll for it to travel, so it would read as a jitter
   rather than a device. Worth revisiting only if the list grows. */
@media (min-width: 900px) {
  /* 🔴 22rem, AND THE THREE-LINE TITLE IS THE CHOICE, NOT AN ACCIDENT.
     Swept 22-30rem: the heading holds THREE lines up to 26rem and snaps to two
     at 28rem (measured — its widest line goes 233px -> 434px there). Brees
     picked the narrow column, so "Approach" sits on its own third line on
     purpose and the rows keep the width: 680px of toggle against 584px at
     28rem, which is ~100px more answer measure.
     ⚠️ DO NOT "FIX" THE WRAP by widening this. Nudging it to 28rem silently
     buys the two-line title back and takes that 100px off every answer. */
  .wapp__split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 22rem);
    column-gap: clamp(32px, 5vw, 88px);
    /* 🔴 NO `align-items: start` HERE — IT WOULD BREAK THE COLOUR SPLIT.
       It was set while this was a plain two-column layout, and it has to go now
       that the right cell paints the darker panel: `start` sizes a cell to its
       CONTENT, so the fill would stop where the title stops and end short of
       the section. seo-content.css records this exact failure on .proofcase,
       measured at 140px short. Default stretch is load-bearing.
       The title still top-aligns — inside .wapp__aside, which is what the
       wrapper is for. */
  }

  .wapp__split > .wapp__aside {
    grid-column: 2;
    grid-row: 1;
    padding-inline-start: var(--s-5);
  }

  .wapp__split > .wapp__list {
    grid-column: 1;
    grid-row: 1;
  }

  /* 🔴 THE TITLE IS CENTRED IN THE PANEL AND SET LARGER — Brees, 9/2:
     "Lots of top space, title isn't centered. I think we can make the title
     larger here and really use typography design."

     Measured before: 48px type filling 153px of a 740px panel, with 178px
     above it and 409px of dead space below. A small heading marooned at the
     top of a tall dark panel, which is what read as unpolished.

     ⏹ THIS SUPERSEDES THE 8/28 TOP-ALIGN DECISION recorded above — "the
     heading's cap-height sits on the same line as the first row's hairline".
     That alignment was real, but it is what pinned the title to the top of a
     panel that is now half again as tall as its content. Brees asked for
     centred with the section in front of him; his newer call wins. The note
     above is kept so nobody "restores" the old behaviour as a bug fix.

     ⚠️ `align-content`, NOT `align-items`, and the wrapper stays stretched.
     The comment on .wapp__split explains why the cell must not shrink to its
     content: the darker fill is painted by this cell, so a shrunk cell ends
     the panel short of the section. Centring the CONTENT inside a stretched
     cell keeps the fill full-height. */
  .wapp__aside {
    display: grid;
    align-content: center;
  }

  /* 🔴 THE SIZE IS CAPPED BY THE THREE-LINE WRAP, WHICH IS BREES' OWN CHOICE.
     The note on .wapp__split is explicit: the 22rem column exists so the title
     holds THREE lines and "Approach" sits on its own, and widening it to buy a
     two-line title costs ~100px of answer measure on every row. Growing the
     TYPE has the same danger from the other direction — push it far enough and
     "The Digital" breaks and the title goes to four lines.
     Measured: at 48px the widest line runs 233px inside ~288px of text column,
     so there is about 1.23x of headroom. 3.5rem (56px) sits inside that with
     room to spare; the clamp's vw term keeps it off the edge between
     breakpoints. Anything larger needs the wrap re-checked, not assumed. */
  /* 🔴 THE TITLE ALIGNS TO THE PANEL, NOT TO THE PAGE SPINE — Brees, 9/2:
     "would it look good right aligned and a bit larger? so it's like a
     statement?"

     It was already `text-align: right`. What made it not LOOK right-aligned is
     that it aligned to the container's right edge at 1276 while the dark panel
     bleeds on to the viewport at 1440 — 164px of empty panel to its right, so
     the type read as floating in the middle of the fill.

     This pulls the cell's right edge out to one --s-5 short of the viewport,
     so the heading right-aligns on a real optical margin of 64px against the
     panel's own edge. The LEFT edge of the cell does not move, so the panel's
     rounded left corners stay exactly where they are.
     ⚠️ `max-width: none` matters as much as the offset. With the 20ch cap the
     box shrank to its longest line, so `text-align: right` landed wherever
     that line happened to end — measured 1310 at 72px and 1382 at 88px, a
     margin that changed with the type size. Filling the cell fixes the edge
     and lets only the size vary. */
  .wapp__aside {
    margin-right: calc(-1 * max(0px,
      (100vw - var(--container-max)) / 2 + var(--container-pad) - var(--s-5)));
  }

  .wsite-approach__head {
    margin-bottom: 0;
    text-align: right;
    max-width: none;
    /* 🔴 72px IS THE MEASURED CEILING, NOT A PREFERENCE. Rendered at 72 / 88 /
       100 against the widened cell: 72 keeps the three-line wrap and lands the
       right margin on exactly 64px; 88 breaks to four lines and eats into the
       margin (58px); 100 comes within 4px of the viewport edge and only looks
       contained because the section clips. The cap is 4.5rem for that reason.
       ⚠️ Raising it means re-rendering, not re-reasoning.

       🔴 THE vw TERM IS 4.6, NOT 5, AND THE DIFFERENCE IS 1280px. At 5vw the
       widest line measured 301px inside 308px of cell there — three pixels of
       slack, because that width gets the smallest bleed offset while the type
       is still near full size. It passed, and it would have broken on any
       machine whose font metrics round differently. 4.6vw gives 31px of slack
       at 1280 and still reaches the 4.5rem cap by ~1570. */
    font-size: clamp(3rem, 4.6vw, 4.5rem);
    /* Tighter than the body scale on purpose. At display size the default
       1.06 leading opens gaps between three short lines and they stop reading
       as one stacked block; closing them makes it a single mass, which is what
       makes this a typographic moment rather than just bigger text. Tightened
       again with the size bump — 0.95 was set at 56px and reads loose at 72. */
    line-height: 0.9;
    letter-spacing: -0.035em;
  }
}

/* --------------------------------------------------------------------------
   THE COLOUR SPLIT — a darker panel behind the title, ported from the SEO
   page's case-study rail. Brees, 8/28: "what if we do a split color like we
   did /products/seo-content/".

   🔴 MIRRORED, NOT COPIED: that rail is on the LEFT and bleeds left; this one
   is on the RIGHT, behind the title, and bleeds right. Everything else is the
   same mechanic, including the reasons.
   -------------------------------------------------------------------------- */

/* 🔴 THE SECTION'S BLOCK PADDING MOVES TO THE COLUMNS. `.section` sets
   padding-block; with the panel full-bleed, that padding would sit OUTSIDE the
   dark fill and the two-tone band would float as a stripe with indigo above
   and below it. The columns pad themselves so the fill runs the whole height. */
@media (min-width: 900px) {
  .wsite-approach { padding-block: 0; }

  .wapp__aside,
  .wapp__list { padding-block: var(--section-pad); }
}

/* 🔴 `clip`, NOT `hidden`, AND IT IS LOAD-BEARING TWICE.
   `.section--indigo` carries `border-radius: var(--r-bleed)`, so without
   clipping the panel's square corner pokes out past the rounded section edge.
   And `overflow: hidden` would make this a scroll container, which silently
   kills `position: sticky` on any descendant — this section has none today,
   but the sheet's own rule on `.wsite-problem` says the same thing, and the
   hero lost a day to exactly that. */
.wsite-approach { overflow: clip; }

/* [!] AT LEAST 80vh - BREES, 9/9. Measured 668px at 1440x900, which is 74vh,
   not the ~50% it reads as.

   [!] min-height, NOT PADDING, AND THAT IS LOAD-BEARING. This is one of the
   two sections corner-check lists in PAY_ON_COLUMNS: it declares zero block
   padding so its full-bleed panel can run the section's whole height, and the
   corner payback goes to .wapp__list instead. Buying height with padding here
   fails that check and floats the panel as a stripe.
   [i] `align-content: center` so the extra height opens above AND below the
   content rather than dropping it all under a top-aligned block. */
@media (min-width: 900px) {
  .wsite-approach {
    min-height: 80vh;
    align-content: center;
  }
}

@media (min-width: 900px) {
  .wapp__aside {
    /* ⚠️ ITS OWN STACKING CONTEXT, so the bleeding pseudo-element can sit at
       z-index -1 without dropping behind the SECTION's own background and
       disappearing entirely. */
    position: relative;
    isolation: isolate;
  }

  /* 🔴 A PSEUDO-ELEMENT, NOT `background-color` ON THE CELL. The cell lives
     inside the 1240px container, so its box stops well short of the viewport
     edge — a background would end there and the two-tone band would be a
     floating stripe rather than running off the side.

     `right: -100vw` is deliberately far more than needed and is CUT by the
     `overflow: clip` above. Computing the exact bleed would mean
     (100vw - container)/2 arithmetic that the SEO page got wrong twice; a
     value that over-reaches and is clipped cannot be off by a pixel.
     ⚠️ `inset-block: 0` ties the fill to the CELL, which stretches — so it
     always reaches the section's top and bottom edges. */
  .wapp__aside::before {
    content: "";
    position: absolute;
    z-index: -1;
    inset-block: 0;
    left: 0;
    right: -100vw;
    /* Ink at 22% over indigo, about #46475d — the SEO rail's own value.
       ⚠️ DEFINED LOCALLY, NOT AS A GLOBAL TOKEN. It is one panel on one
       section; adding it to base-v3.css would put a colour in the shared
       palette that the brand does not have. Same reasoning the SEO sheet
       gives for its copy of this value. */
    background-color: rgba(47, 47, 53, 0.22);
  }
}

/* 📌 IT DARKENS, SO EVERY CONTRAST FIGURE FOR THIS SECTION IS A FLOOR, NOT AN
   EXACT VALUE — white on the panel measures HIGHER than white on the section.
   The toggles are NOT on the panel, so petal's 4.74 on plain indigo is
   unchanged and is still the tightest pair here. */

.wsite-approach__list {
  display: grid;
  gap: var(--s-4);
}

/* ⏹ THE THREE-COLUMN LIST AND ITS HAIRLINE SEPARATORS ARE RETIRED — 8/28.
   The principles are three toggles now. The old rules went with them rather
   than sitting orphaned; `.wsite-principle*` no longer exists in the markup.
   📌 The vertical separator trick they used (a border on the FOLLOWING item, so
   it is exactly as tall as its own column with nothing measured) is still the
   right answer for a column layout — the SEO services section still uses it. */

/* ROWS SEPARATED BY HAIRLINES, WHICH IS THE ONE CONTAINER THE HOUSE RULE
   ALLOWS. No boxes, no fills, no rounded panels — the rule between rows is
   what separates a principle from the next, exactly as the FAQ does it. */
.wapp__item {
  border-top: 1px solid rgba(243, 238, 232, 0.20);
}

.wapp__list > .wapp__item:last-child {
  border-bottom: 1px solid rgba(243, 238, 232, 0.20);
}

/* 🔴 oat AT 0.20 MEASURES 1.62 ON INDIGO, over the ~1.35 shape floor — the same
   value the retired column separators used, kept for exactly the same reason.
   ⚠️ NOT 0.14 (1.40): that is the pricing band's value on a much smaller
   element, and it goes invisible at this length. */

.wapp__q {
  margin: 0;
  font-size: var(--fs-body);
  font-weight: 500;
}

/* The control. Mirrors the FAQ button's shape so the two accordions on this
   page behave and read identically — body face, not the display face, because
   these are controls rather than headings. */
/* 🔴 THE OPEN ROW'S TITLE IS PETAL — Brees, 8/28. State carried by COLOUR and
   the marker, never by a box or a fill; the house rule keeps copy out of
   containers, and an open accordion row is exactly where a filled "active"
   panel usually creeps in.

   ⚠️ PETAL ON INDIGO IS 4.74:1 — IT PASSES, BUT IT IS THE TIGHTEST PAIR ON THE
   PAGE. The 4.5 body floor applies here because these controls are set in the
   body face at --fs-h3, and this sheet's header records the same figure.
   🔴 DO NOT REUSE THIS COLOUR FOR THE ANSWER TEXT. --oat-75 is 4.77 and is what
   the paragraphs use; petal at a smaller size or a lighter weight has no margin
   left. It is on the control only, which is a single short line at h3 size.

   The transition matches the marker's so the row changes state as one thing. */
.wapp__q button[aria-expanded="true"] {
  color: var(--petal);
}

.wapp__q button {
  transition: color var(--d-marker) var(--ease-out);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-3);
  width: 100%;
  padding-block: var(--s-3);
  color: var(--white);
  font-family: var(--font-body);
  font-size: var(--fs-h3);
  font-weight: 400;
  letter-spacing: 0;
  line-height: 1.3;
  text-align: left;
}

.wapp__marker {
  position: relative;
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin-top: 6px;
}

/* 🔴 THE ARMS ARE oat, NOT --ink. This is the one thing that could not be
   inherited from the FAQ: its marker draws in `--ink`, which is very nearly
   the indigo ground here and would have left an invisible control. */
.wapp__marker::before,
.wapp__marker::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  background-color: var(--oat);
  border-radius: 1px;
  transition: transform var(--d-marker) var(--ease-out);
}

.wapp__marker::before { width: 14px; height: 2px; transform: translate(-50%, -50%); }
.wapp__marker::after  { width: 2px; height: 14px; transform: translate(-50%, -50%); }

/* 🔴 ROTATE THE ARMS, NOT THE BOX — carried over from the FAQ, where this was
   a real fix. An 18px square rotated 45deg has a 25px bounding box, so every
   expanded row reported 4px of phantom horizontal overflow and dirtied
   scrollWidth on every ancestor. Rotating the two pseudo-element arms gives an
   identical "x" while the marker's own box stays exactly 18px. */
.wapp__q button[aria-expanded="true"] .wapp__marker::before,
.wapp__q button[aria-expanded="true"] .wapp__marker::after {
  transform: translate(-50%, -50%) rotate(45deg);
}

/* oat-75 measures 4.77 on indigo — clears the 4.5 body floor. The 68ch cap is
   the FAQ's: the ROWS span the container, the PROSE does not, or answer lines
   run past 110 characters on a wide screen. */
.wapp__a {
  margin: 0;
  padding-bottom: var(--s-3);
  max-width: 68ch;
  color: var(--oat-75);
}

/* Inline links inside body copy. --lime is 5.44 on indigo, the strongest
   accent available there, and it is underlined rather than colour-only so the
   link is not signalled by hue alone.
   ⚠️ On light grounds this same class flips to --ink; see the override below
   the costs section. */
.wsite-inline-link {
  color: var(--lime);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.wsite-inline-link:hover { text-decoration-thickness: 2px; }


/* ==========================================================================
   4. HOW IT WORKS — white ground, three steps
   ========================================================================== */

/* ⏹ THE FIRST BUILD OF THIS SECTION WAS THREE PLAIN COLUMNS, AND IT WAS
   WRONG. Brees, 8/28: "it does not have hardly any of the design elements
   that we've already established on the other creative pages."

   It had a small letter-spaced "01/02/03" over a bare heading and body —
   correct tokens, none of the brand's actual vocabulary. What replaced it is
   the SAME COMPONENT the other two pages already use, ported here:
       Call Insights  .how__track     > .how__node.arch.v3-seq
       SEO & Content  .aspects__track > .aspects__node.arch.seo-seq
       here           .wsteps__track  > .wstep.arch.wsite-seq
   Same .arch dome, same three numeral PNGs, same centred title.

   📌 THE ARCH IS THE BRAND'S CORE FORM, not a card. brand-devices.md
   section 1: the icon is two arch-figures, and "the 24px radius in the layout
   system is a descendant of this shape". This is also why a row of arches is
   not a no-boxes violation — it is the named "whole section of panels as a
   system" case, and both sibling pages ship it. */

/* CENTRED — Brees, 8/28. The three steps below it are a symmetrical row of
   arches, so a left-aligned title sat off-axis against them.
   ⚠️ `margin-inline: auto` needs a width to centre against; the heading is a
   block that already fills the container, so `text-align` is what does the work
   here. No max-width cap on this one, unlike the Approach heading — "How it
   works" is three words and needs none. */
.wsite-steps__head {
  margin: 0 0 var(--s-5);
  text-align: center;
}

.wsteps__track {
  position: relative;
  display: grid;
  gap: var(--s-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 900px) {
  .wsteps__track {
    grid-template-columns: repeat(3, 1fr);
    /* ⚠️ STRETCH, NOT START. `start` lets each card take its own height, so
       three cards with different copy lengths end at three different depths
       and the line's baseline has nothing straight to run along. The dome
       stays a true semicircle either way because its radius is driven by
       WIDTH, not height. Copied from .aspects__track, where this was the
       fix, not a preference. */
    align-items: stretch;
  }
}

@media (min-width: 1024px) {
  /* 44, not 24 — the outer gap has to read as clearly wider than the gaps
     inside a card; Brees called out the mismatch at 24 on Call Insights.
     🔴 THE LINE MEASURES THIS GAP rather than carrying its own constant, so
     changing it moves the line too. Do not introduce a second number. */
  .wsteps__track { column-gap: 44px; }
}

.wstep {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
}

/* IN FLOW, not absolutely positioned — mirrors .how__num / .aspects__num
   exactly. The arch's own `padding-top: var(--arch-dome)` drops the numeral
   into the dome, and because the dome is a 999px radius the curve spans
   roughly the top half of the card, so an in-flow numeral lands inside it at
   every column width. Positioning it absolutely would pin it to a fixed
   offset the dome no longer follows when the column resizes. */
.wstep__num {
  display: block;
  margin-bottom: var(--s-3);
  line-height: 0;
  text-align: center;
}

/* 56px, the same height .how__num-img and .aspects__num-img use. The three
   source files have different intrinsic widths (41 / 65 / 60) and one shared
   vertical crop, so sizing by HEIGHT is what keeps their baselines aligned. */
.wstep__num-img {
  display: inline-block;
  height: 56px;
  width: auto;
}

/* Centred, like .how__step-title and .aspects__title. The numeral and title
   are centred in the dome because they are short; the body stays left at a
   readable measure, because forty words of centred type is a legibility cost,
   not a style. */
.wstep__head { margin-bottom: var(--s-2); text-align: center; }

.wstep .t-body {
  margin: 0;
  font-size: var(--fs-small);
}


/* ==========================================================================
   5. COSTS — oat ground, the paired comparison table
   ========================================================================== */

.wsite-costs__head { margin: 0 0 var(--s-5); }

/* 🔴 A KEYBOARD-REACHABLE SCROLL REGION, AND THIS IS NOT DECORATION.
   A table that scrolls horizontally must be reachable by keyboard or its
   overflowed columns are unreachable without a mouse. tabindex="0" +
   role="region" + aria-labelledby is the pattern; the caption names it.
   ⚠️ THE SEO PAGE LOST EXACTLY THIS when its table became a band + cards on
   8/27, and nothing has since verified what replaced it is reachable. Do not
   remove this wrapper without checking what the replacement does with a
   keyboard. */
.wsite-costs__scroll {
  overflow-x: auto;
  margin-bottom: var(--s-5);
}

/* ⏹ NO :focus-visible OVERRIDE HERE — one was written and REMOVED 8/28.
   It restated `outline: 3px solid var(--ink)` with `outline-offset: 4px`,
   which looked harmless and was not: base-v3.css pairs that outline with a
   `box-shadow: 0 0 0 2px var(--white)` band and sets offset 2px precisely so
   the two bands sit ADJACENT with no ground showing between them. Pushing the
   offset to 4px opened a 2px gap between the ink outline and the white band
   and broke the dual-tone indicator the sheet spends 25 lines defending.
   The global rule already styles this element correctly. Add nothing. */

/* --------------------------------------------------------------------------
   THE TABLE — DESIGN 1d (Brees' canvas, 8/31). PRESENTATION ONLY.

   Every string in the markup is unchanged; this block is what makes it look
   like 1d. Five things moved from the 8/28 table:
       1. the row-label column is squeezed to zero width (labels still read)
       2. the column headers became petal / lime pills
       3. the price gained a lighter, smaller unit beside the figure
       4. a heavy ink rule closes the price block, unbroken across the gutter
       5. the row hairlines BREAK at the gutter instead of running through it

   🔴 STILL A REAL <table>, AND STILL FOR ALIE'S REASON. Her build note asks
   that each Build item sit opposite its Subscription counterpart. 1d keeps the
   rows aligned across the gutter, so the pairing survives. Do NOT "simplify"
   this into two <ul>s or two panels — that is variant 1a, and it drops the
   pairing that is the section's whole argument.
   -------------------------------------------------------------------------- */

.wsite-table {
  /* The gutter between the two columns. It is PADDING ON THE LEFT COLUMN, not
     border-spacing: border-spacing also insets the table's outer edges, and
     with the zero-width label column in front it would inset the left edge
     twice. Everything below that needs to know the gutter reads this var. */
  --wcol-gap: var(--s-5);

  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

.wsite-table th,
.wsite-table td {
  padding: 0;
  vertical-align: top;
}

/* 🔴 ZERO WIDTH, NEVER display:none. These cells still carry
   <th scope="row"> row labels for screen readers ("Price", "What it's for",
   "What's included"). display:none would remove them from the accessibility
   tree and every data cell would lose its row context. Empty + zero-width is
   invisible to the eye and intact to a reader. */
.wsite-table tbody th,
.wsite-table thead td {
  width: 0;
  padding: 0;
}

.wsite-table thead th,
.wsite-table tbody td {
  width: 50%;
  padding: var(--s-3) 0;
  color: var(--ink);
}

/* The left data column carries the gutter. `:first-of-type` and NOT
   `:first-child` — the first child of every body row is the hidden <th>. */
.wsite-table thead th:first-of-type,
.wsite-table tbody td:first-of-type { padding-right: var(--wcol-gap); }

/* -- the hairlines --------------------------------------------------------
   🔴 DRAWN WITH ::after, NOT border-bottom, AND THAT IS THE WHOLE POINT OF 1d.
   A border spans the cell's padding box, and the gutter IS padding on the left
   cell — so a border-bottom would always run straight through the gutter, which
   is exactly what the 8/28 table did. `right: var(--wcol-gap)` pulls the line
   back to the content edge and opens the break 1d shows.
   ⚠️ position:relative on a <td> is well-defined and widely supported; this is
   not a layout hack.
   🔴 THE COLOUR IS RE-MEASURED, NOT CARRIED OVER. Ink at 0.20 composites to
   1.47 on --white (scripts/pill-contrast.js, 8/31), over the ~1.35 shape floor.
   The 8/28 note recorded 1.48 for the same pair; the 0.01 is rounding in the
   compositing step, not a change. --line-ink (0.16) is still the weaker choice. Re-measure if this section's ground ever changes;
   a carried-over hairline is how one goes invisible unnoticed. */
.wsite-table tbody td { position: relative; }

.wsite-table tbody td::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: rgba(47, 47, 53, 0.20);
}

.wsite-table tbody td:first-of-type::after { right: var(--wcol-gap); }

/* 🔴 THESE THREE SELECTORS CARRY `.wsite-table tbody tr` AND THAT PREFIX IS
   LOAD-BEARING, NOT TIDINESS. The base line above is
   `.wsite-table tbody td::after` — specificity (0,1,3). A bare
   `.wsite-table__rulerow td::after` is only (0,1,2) and SILENTLY LOSES: the
   rule renders as an ordinary 1px hairline that breaks at the gutter, and the
   price row keeps a line it should not have. Both shipped that way for one
   measurement cycle and were caught by scripts/costs-1d-check.js, not by eye.
   If you add another row modifier, match this shape. */

/* The price row closes with the heavy rule below it, so it carries no line. */
.wsite-table tbody tr.wsite-table__pricerow td::after { content: none; }

/* THE HEAVY RULE — the one line that runs UNBROKEN across the gutter, because
   it separates the header block from the list rather than one row from the
   next. `right: 0` lets the left cell's line run through its own gutter padding
   to meet the right cell's, and the two read as one rule. */
.wsite-table tbody tr.wsite-table__rulerow td {
  padding-bottom: var(--s-3);
}

.wsite-table tbody tr.wsite-table__rulerow td::after {
  height: 2px;
  background: var(--ink);
}

.wsite-table tbody tr.wsite-table__rulerow td:first-of-type::after { right: 0; }

/* -- the pills ------------------------------------------------------------
   ⚠️ A RADIUS AND A FILL, AND THEY ARE DELIBERATE. The house no-boxes rule is
   about panels that wrap content; these are type labels the width of their own
   words, the same device the pills elsewhere on the site use. They do not box
   anything.
   Ink on petal and ink on lime are both measured well clear of AA — see
   scripts/pill-contrast.js. Colour is never the only signal: the words "The
   Build" and "The Subscription" carry the meaning on their own. */
.wsite-pill {
  display: inline-block;
  padding: 6px 14px;
  border-radius: 999px;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
}

.wsite-pill--build { background: var(--petal); }
.wsite-pill--sub { background: var(--lime); }

/* -- the price ------------------------------------------------------------
   The two cells a reader decides from, so they take the largest type in the
   section after the headline. .t-tabular is on them in the markup for lining
   figures. */
.wsite-table__price {
  font-size: clamp(1.75rem, 2.6vw, 2.25rem);
  font-weight: 700;
  line-height: 1.1;
  white-space: nowrap;
}

/* "‚ one time" / " per month" — the unit steps back so the figure reads first.
   ⚠️ Its leading whitespace/comma lives INSIDE the span in the markup, and the
   verify gate depends on there being no whitespace at the join. See the comment
   over the price row in index.html before reformatting either. */
.wsite-table__unit {
  font-size: var(--fs-body);
  font-weight: 400;
  letter-spacing: 0;
}

/* ⏹ NO STACKING MEDIA QUERY, AND ONE WAS WRITTEN AND REMOVED ON 8/31.
   It read well in theory — below 700px each cell became a block — and it broke
   the section's meaning: a <table> stacks BY ROW, so the thead row put both
   pills together, then the price row put both prices together. A reader got
   "The Build / The Subscription / $2,750 / $199", with every label detached
   from its number, and the unlabelled item rows gave no clue which column an
   item belonged to. Measured at 375px, not reasoned.
   📌 The narrow-screen answer was already in the markup: .wsite-costs__scroll
   is an overflow-x region with tabindex="0" + role="region", built for exactly
   this and keyboard-reachable. Let it do its job.
   🔴 If a stacked mobile layout is ever wanted, it needs the column label
   REPEATED PER CELL, not a display:block sweep — and that is a copy decision
   (it invents visible strings), not a CSS one. */

/* "Need more words?" — DESIGN 1d: a sky panel, three columns.
   ⚠️ THIS IS A DELIBERATE EXCEPTION TO THE STANDING NO-BOXES RULE, taken on
   Brees' instruction 8/31 against the 1d artboard. Recording it because the
   rule has real history: direction-d-v3.css removed a white card from the
   closing CTA on 8/24 specifically to get back ONTO the rule, with a note
   saying nothing there should grow a background, a radius or a shadow again.
   That note still governs .cta-quote. It does not govern this block any more.
   📌 The radius is 24px because that is the house value — brand-devices.md
   section 1 derives it from the arch, the brand's core form. Not a new number.
   Ink on --sky-soft measures 11.54 (scripts/pill-contrast.js, 8/31). */
.wsite-more {
  display: grid;
  grid-template-columns: minmax(180px, 0.9fr) minmax(260px, 1.5fr) auto;
  align-items: center;
  gap: var(--s-3) var(--s-4);
  padding: var(--s-4) var(--s-5);
  border-radius: 24px;
  background: var(--sky-soft);
}

.wsite-more__head { margin: 0; }
.wsite-more__body { margin: 0; color: var(--ink); }
.wsite-more__link { justify-self: end; text-align: right; }

/* Three columns need ~900px before the middle one drops under 40 characters.
   Below that the panel stacks — and stacking is safe HERE, unlike in the table,
   because these three blocks are a heading, its body and its link, already in
   reading order. Nothing is a label that loses its value. */
@media (max-width: 899px) {
  .wsite-more {
    grid-template-columns: 1fr;
    padding: var(--s-4);
  }

  .wsite-more__link { justify-self: start; text-align: left; }
}

/* 🔴 THE INLINE LINK FLIPS ON LIGHT GROUNDS. --lime is 1.10 on white and 1.17
   on oat — invisible. On every light section it is full --ink (11.53 on oat,
   13.30 on white), still underlined so it never depends on colour alone. */
.wsite-costs .wsite-inline-link,
.wsite-steps .wsite-inline-link,
.wsite-problem .wsite-inline-link {
  color: var(--ink);
}


/* ==========================================================================
   6. PROOF — indigo ground
   ⚠️ .wproof, NOT .proof. .proof is position:sticky and is queried by
   initProofPin() in direction-d-v3.js, which this page loads.
   ========================================================================== */

.wproof__head {
  max-width: 20ch;
  margin: 0 0 var(--s-3);
  color: var(--white);
}

.wproof__lede {
  max-width: 54ch;
  margin: 0 0 var(--s-5);
  color: var(--oat-75);
}

/* ⚠️ THE QUOTE NOW CARRIES A PETAL LEFT RULE, AND THAT REVERSES AN 8/28
   DECISION. The note this replaces read: "No quote card and no left border —
   the house rule names coloured left borders on callouts specifically."
   Brees' 8/31 artboard shows the rule, so the design owner has overridden the
   house rule here. Recorded rather than quietly dropped, because the next
   person to read the house rule will otherwise think this is a mistake.
   📌 It is a rule, not a card: no background, no radius, no shadow. The quote
   still sits on the section ground.
   Petal on indigo measures 4.74 (scripts/pill-contrast.js, 8/31) — the rule is a
   non-text graphic and needs 3.0, so it clears with room. */
.wproof__quote {
  max-width: 34ch;
  margin: 0 0 var(--s-4);
  padding-left: var(--s-3);
  border-left: 3px solid var(--petal);
}

.wproof__quote blockquote { margin: 0; }

.wproof__quote blockquote p {
  margin: 0;
  font-size: var(--fs-h3, 1.5rem);
  line-height: 1.35;
  /* Full oat, 7.01 on indigo. The quote is the loudest thing in the section. */
  color: var(--oat);
}

@media (min-width: 900px) {
  .wproof__quote { max-width: 46ch; }
}

.wproof__by {
  margin: var(--s-3) 0 0;
  font-size: var(--fs-small);
  color: var(--oat-75);
}

.wproof__cta { margin: 0; }

/* ⏹ .wproof__cta-label IS GONE. It styled a "See the work:" span that sat
   OUTSIDE the link; 8/31 folds the whole phrase into one .btn--onDark, so the
   span no longer exists. Nothing else used the class. */

/* -- the two-column layout (8/31) ----------------------------------------
   The copy column and the render sit side by side from 900px up. Below that
   the render drops UNDER the copy rather than beside it — it is decorative
   (aria-hidden), so it is the half that should yield when space runs out.
   ⚠️ .wproof__grid is put ON .container, so the section keeps the site's
   single container width and gutters instead of nesting a second one. */
.wproof__grid {
  display: grid;
  gap: var(--s-5);
}

/* [!] FLIPPED - BREES, 9/9: "let's flip it, including the background. Image
   and shape are on the left; text is on the right."

   The DOM order is unchanged (copy first, then media) so the reading order and
   the tab order still go heading -> lede -> quote -> CTA -> render. Only the
   PAINTED sides swap, via grid-column. [!] Do not do this by reordering the
   markup: it would put the decorative render ahead of the H2 for a screen
   reader and for the keyboard.

   The 1.05fr stays with the MEDIA, which is now column 1 - the render is the
   wider half in the artboard and flipping the ratio with it is the point. */
@media (min-width: 900px) {
  .wproof__grid {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    align-items: center;
  }

  .wproof__media { grid-column: 1; grid-row: 1; }
  .wproof__copy  { grid-column: 2; grid-row: 1; }
}

/* THE BLEED CIRCLE, TONED DOWN — 8/31, TO MATCH THE ARTBOARD.
   At full --petal the circle read as a hot pink disc that fought the render for
   attention; the artboard shows a muted mauve sitting quietly behind the window.
   0.42 over --indigo composites to about #937496, which is what the mock shows.

   🔴 SCOPED TO .wproof ON PURPOSE. `.shape--petal-lg` is declared in FOUR
   sheets (direction-b, -c, -d, -d-v3) and used by other sections and other
   pages. Editing the class itself would silently restyle all of them. If this
   ever needs to be the global look, change it there deliberately — not from
   here.
   ⚠️ Opacity on the shape, NOT a new colour token: the palette is fixed and
   #937496 is not in it. This stays "petal, quietly", not a sixth colour. */
/* [!] "INCLUDING THE BACKGROUND" - the bleed circle moves with the render.
   direction-d-v3.css declares this class at `top: 4%; right: -6%`, and that
   declaration is shared by four sheets and other pages, so the flip is scoped
   to .wproof and sets `right: auto` explicitly - leaving `right` in place
   alongside `left` would pin the circle to BOTH edges and stretch it. */
.wproof .shape--petal-lg {
  opacity: 0.42;
  right: auto;
  left: -6%;
}

/* 🔴 AND IT IS HIDDEN BELOW 900px, FOR A MEASURED CONTRAST REASON — NOT TASTE.
   The circle is positioned top-right of the SECTION. Once the section stacks,
   the copy column spans the full width and runs straight across it, so the
   reader's actual background for that text is the circle, not --indigo. The
   pair everyone checks (oat on indigo, 7.01) is not the pair on screen.
   Measured with scripts/pill-contrast.js:
       lede over the circle at full --petal   1.34   ← what shipped 8/28
       lede over the circle at 0.42            2.61
       quote over the circle at 0.42           3.40
   All three are under the 4.5 those two elements need. (The headline clears,
   at 30px it is large text and its floor is 3.0 — but it is the only one.)
   📌 SO THIS WAS A PRE-EXISTING DEFECT, not one the 8/31 redesign introduced;
   the redesign only made it visible and the opacity made it less bad. Detected
   by scripts/proof-check.js, which intersects each text box with the circle at
   five widths — run it if the circle's size or position ever changes.
   The circle is aria-hidden decoration, so removing it below 900 costs the
   reader nothing and returns the copy to the 7.01 pair. */
@media (max-width: 899px) {
  .wproof .shape--petal-lg { display: none; }
}

/* 🔴 THIS IS THE QUERY CONTAINER FOR .wr AND IT IS LOAD-BEARING. site-renders.css
   sizes every internal in `em` off `font-size: 3.8cqw`, which resolves against
   the nearest ANCESTOR container. Without this line there is no container, the
   unit falls back to the viewport, and the render paints roughly 4x too large.
   The project has been bitten by this twice — once in the hero, once in the
   flying tile. Do not move container-type onto .wr itself; an element cannot
   query itself. */
.wproof__media {
  container-type: inline-size;
  min-width: 0;
}


/* 🔴 SECTIONS CARRYING A BLEED SHAPE MUST CLIP, AND THIS WAS A REAL BUG.
   `.shape--sky-lg` / `.shape--petal-lg` are large circles positioned to bleed
   off the section edge. Added on 8/28 without clipping, `.shape--petal-lg` in
   .wproof reached x=1535 against a 1448px viewport and gave the WHOLE PAGE an
   87px horizontal scrollbar. Measured, not noticed by eye.

   ⚠️ `overflow-x: clip`, NOT `overflow: hidden` — the project's own rule
   (direction-d-v3.css, 8/18). `hidden` establishes a scroll container, which
   silently kills `position: sticky` on any descendant and cuts a bleeding
   circle flat where a colour step happens. `clip` contains without either.
   ⚠️ NOT on .wsite-steps — that section hosts the routing line, whose
   SVG is deliberately `overflow: visible` so the rail can run past the
   section edge. It carries no bleed shape, so it needs no clip. */
.wsite-problem,
.wproof {
  overflow-x: clip;
}


/* ==========================================================================
   7. THE ROUTING LINE + .wsite-seq
   Added 8/28 with the continuity pass.
   ========================================================================== */

/* The line's host. Absolute inside the section (which is position:relative),
   z-index 1 so it paints ABOVE the section ground and BELOW .container's
   z-index 2 — the copy always stays over the line. Identical to .seotrack. */
.wtrack {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

.wtrack svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* 🔴 A QUIET WARM NEUTRAL, NEVER A BRAND COLOUR. brand-devices.md section 2
   is explicit and says the build brief has this backwards: the stroke is
   ~#D9D3CA and "all colour lives in the dots" sitting on the line. A coloured
   rail reads considerably louder than the real system.
   --v3-track-on-oat is that neutral, and the steps section is on oat. */
.wtrack__rail { stroke: var(--v3-track-on-oat); }


/* .wsite-seq — THIS PAGE'S LINE-DRIVEN REVEAL CLASS.

   🔴 DELIBERATELY NOT .v3-seq AND NOT .seo-seq. Each is revealed only by its
   own page's track script; either one here would be opacity:0 FOREVER. A
   separate name per page makes that impossible to get wrong.

   🔴 AND DELIBERATELY NOT .reveal. `.reveal` fires from shared-v3.js's
   IntersectionObserver the moment an element enters the viewport, which is a
   DIFFERENT CLOCK from the line's. With both attached the cards appear first
   and the line arrives at boxes that are already there — the opposite of the
   effect, and a bug already paid for once on the Call Insights deliverable
   section.

   ⚠️ HIDDEN ONLY UNDER html[data-js="on"]. With JS off nothing draws the line,
   so nothing would ever add .is-in and the three steps would be blank. */
html[data-js="on"] .wsite-seq {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--d-reveal) var(--ease-out),
              transform var(--d-reveal) var(--ease-out);
}

html[data-js="on"] .wsite-seq.is-in {
  opacity: 1;
  transform: none;
}

/* The measuring pass sets this on <html> so the script can read final card
   geometry without the 14px offset skewing it. Mirrors html.seo-measuring. */
html.w-measuring .wsite-seq {
  opacity: 1;
  transform: none;
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  html[data-js="on"] .wsite-seq {
    opacity: 1;
    transform: none;
    transition: none;
  }
}


/* ==========================================================================
   8. THE HERO GALLERY — the unfurling render wall
   Added 8/28. Brees picked this from the two mockups in `_compare/`.
   Mechanic + numbers: see assets/js/hero-gallery.js.
   ========================================================================== */

/* 🔴 THIS SECTION IS NOT `.hero`, AND THAT IS LOAD-BEARING.
   `.hero` carries `overflow: hidden`, which establishes a scroll container and
   KILLS `position: sticky` on every descendant — the pane below would simply
   never stick, and nothing would look broken enough to notice quickly. It is
   the same reason this project writes `overflow-x: clip` rather than `hidden`
   everywhere else. So this hero does not reuse `.hero`, `.hero__grid`,
   `.hero__copy` or `.wsite-stage`.
   ⚠️ Do NOT "tidy" this back onto `.hero` to match the sibling pages. */
.wsite-hero {
  position: relative;
  background-color: var(--white);
}

/* The scroll track. Its height IS the animation's duration — the whole
   unfurl is mapped across it.
   ⚠️ 190vh. The 21st original is 350vh, which is a MID-PAGE section a reader
   has already committed to; this is a HERO. 350 -> 240 on 8/28 to get to "The
   problem" sooner, and 240 -> 180 on 8/31 (Brees: "feels like a lot of
   scrolling ... shorten without making it feel fast").

   📌 180vh WAS TRIED FIRST AND WAS WRONG. At 180 the unfurl's share works out
   to 62vh against the old 70vh — a 12% speed-up — because the phase split is
   a FRACTION of the scroll, so the track height and the split have to be
   solved together. 190 is the height at which 0.78 of the scroll reproduces
   the old unfurl distance. The figure was written down before it was
   measured, and measuring is what caught it.

   🔴 THIS NUMBER AND THE PHASE SPLITS IN hero-gallery.js ARE ONE SETTING.
   The track is always one viewport taller than the scroll it produces, so
   190vh spans 90vh of scroll where 240vh spanned 140vh. The splits were
   re-cut in the same pass so the UNFURL keeps its rate (1.07 -> 1.08 deg per
   vh, measured) and the reclaimed scroll comes out of the scale and the
   column drift instead. Changing this height alone silently speeds the unfurl
   up by the same ratio — which is the one thing the change was avoiding.
   See the phase-split block in hero-gallery.js for the before/after table. */
.wsite-gal {
  position: relative;
  height: 190vh;

  /* 🔴 THE HERO'S CONTAINER IS WIDER THAN THE SITE'S, AND THE OVERRIDE IS
     SCOPED HERE ON PURPOSE. `--container-max` is a GLOBAL token (base-v3.css:305,
     1240px) consumed by 25 `.container` elements across all four pages, and
     direction-d-v3.css derives a full-bleed offset from it. Changing it at the
     root to widen this one hero would silently reflow the homepage and both
     sibling product pages. Set here, it inherits to the `.container` on
     `.wsite-gal__layout` and reaches nothing else.

     1666px is MEASURED, not chosen: it is the value that puts the copy's left
     edge at 230px on a 1998px viewport, matching the reference Brees supplied.
     (1998 - 1666) / 2 + 64px of container padding = 230. Verified in the
     browser, not just arithmetic.

     📌 THE RAKE WAS NOT THE PROBLEM — AGAIN. The reference reads as bigger,
     more upright, more legible tiles, and the obvious move was to lower
     START_TILT, exactly as was tried and reverted on 8/28. An angle sweep
     scored every value from 0 to 75deg against the reference and came back
     FLAT (best 31.4 vs worst 39.0 mean difference, 60-72% of pixels differing
     at every angle) — no angle matched, because the wall was in the wrong
     PLACE, not at the wrong angle. Container width alone explained the copy
     edge, the measure, the wall's left edge and the tile scale. The tilt is
     untouched at the component's own 75deg. */
  --container-max: 1666px;
  /* BRIEFLY 2000px, REVERTED TO THE COMPONENT'S OWN 1000px. The theory was
     that foreshortening pulled the wall's painted left edge inward and so
     widened the gap beside the copy. MEASURED, IT DOES NOT: with
     `transform-origin: 0% 50%` the left edge is pinned, and the painted gap
     came back as exactly 51px at 2000, 2600 and 3400, and at 75, 60 and 55
     degrees. The gap was the copy COLUMN being wider than the copy itself.
     Perspective was never involved, so it returns to the value in the mockup
     Brees approved.
     NOTE: this is why the number is worth re-measuring rather than reasoning
     about -- the plausible explanation was simply wrong. */
  perspective: 1000px;
  perspective-origin: center top;
  transform-style: preserve-3d;
}

/* 🔴 `top` IS `--hdr-h-min`, NEVER `--hdr-h`. The header is sticky AND
   changes height on scroll (162px → 102px), and direction-d-v3.css is explicit
   that every pinned element keys off the MINIMUM. Pinning to `--hdr-h` would
   leave a 60px gap under the header for the whole scroll once it shrank.
   The height subtracts the same value so the pane is exactly the space below
   the header, not a viewport that runs under it. */
.wsite-gal__sticky {
  position: sticky;
  top: var(--hdr-h-min);
  height: calc(100vh - var(--hdr-h-min));
  /* svh, vh first as fallback (9/17 responsive pass): on iPad Safari 100vh is the LARGEST viewport, so with the
     toolbar showing the bottom of the pinned pane sat under it. Identical on desktop, where svh == vh. */
  height: calc(100svh - var(--hdr-h-min));
  width: 100%;
  overflow: hidden;
  perspective: 1000px;
  perspective-origin: center top;
  transform-style: preserve-3d;
}

/* The two-column layout. Copy left, wall right, one fixed gutter between
   them — this is what replaced the measured push-right. */
.wsite-gal__layout {
  display: grid;
  /* THE COPY COLUMN IS THE COPY'S MEASURE, NOT A FREE 1fr. As `1fr` it took
     594px while the text only ever fills its cap, leaving ~50px of dead column
     before the gutter even started -- part of the white band Brees drew a box
     around. Sizing the track to the measure hands that space to the wall. */
  /* ⏹ THE COPY TRACK NOW CARRIES THE WALL'S INSET, AND THE WALL GAVE IT UP.
     Brees, 8/28: the copy "feels like it's hugging up against the wall... I'd
     really like it to be in the open area there... horizontally and vertically
     centred." Measured, it was 52-84px left of the open area's centre at every
     width, and at 1600px and under the container's left edge is 0 — so the copy
     began just 64px from the viewport edge, which is the hugging.

     🔴 THE FIX MOVES NO PIXELS OF THE WALL. The separation used to be a
     `margin-left` ON THE WALL; that exact distance is now part of the COPY's
     track instead, and the wall's margin drops to zero. Track + gap + 0 lands
     the wall precisely where margin + gap + track did, so the composition Brees
     just approved at 12vw is untouched — the copy simply gained the room to
     centre inside its own column rather than being pinned to its left edge.
     ⚠️ So `--gal-wall-inset` and the wall's margin are ONE value in two places.
     Change the inset and the wall stays put; change both and it moves. */
  --gal-wall-inset: 0px;
  --gal-wall-margin: clamp(93px, 12vw, 200px);
  /* Named, because the copy's centring has to know it — see the padding-left
     below. It was an inline literal and the two silently disagreed. */
  --gal-gutter: clamp(24px, 2.5vw, 44px);
  grid-template-columns:
    minmax(0, calc(var(--gal-copy-w) + var(--gal-wall-inset)))
    minmax(0, 1fr);
  column-gap: var(--gal-gutter);
  /* 🔴 STRETCH, NOT CENTRE — AND THE DIFFERENCE IS NOT SUBTLE. With
     `align-items: center` the wall column sized to its CONTENT instead of the
     pane, so `height: 100%` on the grid resolved against an auto height: the
     wall came out 248px tall in a 576px pane and its flex tiles were crushed
     from ~200px to 63px. The copy is centred on its own below, which is the
     only thing that actually wanted centring. */
  align-items: stretch;
  height: 100%;

  /* 🔴 PRESERVE-3D, OR THE WALL HAS NO PERSPECTIVE AT ALL — AND THIS WAS A
     SILENT REGRESSION. `perspective` on an ancestor applies to its CHILDREN.
     This element was inserted between `.wsite-gal__sticky` (which carries the
     perspective) and `.wsite-gal__grid` when the hero became two columns, so
     the grid stopped being a child of the perspective element. Without
     `preserve-3d` here the chain flattens: the rotation still applies, but as
     a plain vertical squash with NO trapezoid and no depth.

     That is what actually cost the mockup's look. It read as a tidy upright
     card grid rather than a wall raked into the distance, and it was chased
     for two rounds as an ANGLE problem — lowering the tilt, then the tile
     proportions — when the 3D had simply been switched off by a wrapper. */
  transform-style: preserve-3d;
  /* THE ROW MUST NOT GROW WITH THE WALL, AND THIS WAS A REAL REGRESSION.
     Raising the wall to 10 tiles per column made that column ~2000px tall.
     With an implicit `auto` row the layout row grew to fit it, so the copy --
     which centres itself inside its own column -- centred at ~1000px down and
     disappeared off the bottom of the pane entirely. `minmax(0, 1fr)` caps the
     row at the pane height and lets the wall overflow it instead, which the
     pane already clips. */
  grid-template-rows: minmax(0, 1fr);
}

.wsite-gal__grid {
  position: relative;
  z-index: 1;
  display: grid;
  /* A minimum column width so the wall BLEEDS RIGHT rather than shrinking its
     renders. Brees, 8/28: cut off at the right edge of the screen is fine. */
  /* ⏹ THREE, AS THE MOCKUP. Briefly four, to make the tiles read as
     thumbnails rather than slabs -- another compensation for the flattened 3D.
     Perspective does that job properly now. */
  grid-template-columns: repeat(3, minmax(230px, 1fr));
  gap: 10px;
  height: 100%;
  /* Lets the tile stack overflow the capped row rather than compressing it. */
  min-height: 0;
  /* 🔴 BLEEDS TO THE VIEWPORT EDGE. `.container` caps at 1240px and centres;
     this pulls the wall's right edge back out to the window, so the wall runs
     off the screen instead of stopping at the container's gutter. The sticky
     pane's `overflow: hidden` clips it. */
  margin-right: calc(50% - 50vw);

  /* THE WALL SITS FURTHER RIGHT, AND THIS IS WHAT CLOSED THE WHITE WEDGE.
     Brees: "just put everything a little to the right so it fits?"

     He was right and I argued against it first. The reasoning was that the
     wall's left edge is a DIAGONAL under perspective, so moving it right only
     enlarges the triangle beside the copy. True in the abstract, and it misses
     what actually matters: at its old position the wall's ragged upper-left
     corner sat right against the copy, so the empty triangle read as a NOTCH
     in the artwork. Moved clear, the same space reads as an even margin
     between two elements. The gap did not need to be filled, it needed to
     stop looking like a bite out of the wall.

     ⚠️ An earlier trial at 150px did flatten the depth and I called the whole
     idea dead on that basis. That was too quick: at 260 the rake is intact.
     Test the value before rejecting the direction.

     ⏹ NOW 19vw, AND THE BRIEF CHANGED FROM "WHERE" TO "HOW MUCH". The earlier
     rounds moved the wall by feel (18 -> 8 -> 12vw). This one is a coverage
     target: Brees, 8/28, "right now it's about 55% of the screen so let's do
     like 45%."

     🔴 COVERAGE IS A QUESTION ABOUT PAINTED PIXELS, AND THE BOX CANNOT ANSWER
     IT — the wall paints 99-215px outside its layout box. Measured by scanning
     renders with the copy hidden (scripts/wall-coverage-sweep.js +
     coverage-scan.py), at a 36rem measure:

         inset    1440 coverage    1998 coverage
         12vw         48.7%            53.4%
         15vw         46.5%            49.4%
         18vw         44.4%            47.4%
         21vw         42.3%            45.5%

     The target sits at ~18vw on a 1440 and ~21vw on a 1998, and NO SINGLE vw
     HOLDS 45% ACROSS THE RANGE. A flat 19vw was tried and measured 39.3% at
     1280 rising to 47.7% at 1728 — an 8-point spread, because the copy's
     measure is a FIXED 36rem while the inset scales with the viewport, so the
     copy eats a bigger share of a narrow screen.

     🔴 SO THE INSET IS LINEAR IN THE VIEWPORT, NOT PROPORTIONAL TO IT.
     `calc(55vw - 550px)` holds the wall's left edge at ~55% of the viewport
     whatever the fixed 36rem is doing beside it; the floor and ceiling stop it
     inverting on phones and running away past the container cap. Measured:

         viewport   flat 19vw   calc(55vw - 550px)
         1280         39.3%          44.4%
         1366         41.4%          44.7%
         1440         43.6%          45.2%
         1600         47.0%          45.8%
         1728         47.7%          45.4%
         1998         46.8%          46.5%

     ⚠️ THE -550px IS FITTED, NOT DERIVED. It absorbs the 36rem measure, the
     container padding and the wall's ~139px paint overhang at these widths. If
     the measure, the padding or the wall's scale/tilt change, RE-MEASURE with
     scripts/wall-coverage-sweep.js — do not re-derive it in your head.
     📌 Below 1024 the measure is capped by `50vw` and the buttons stack, so
     coverage falls away by design; that range is a different layout, not a
     miss against this target.

     Earlier rounds, kept for the reasoning: 18vw -> 8vw ("like half of that"),
     then 12vw ("a little bit too far to the left").

     ⚠️ THE vw FIGURE UNDERSTATES EVERY ONE OF THESE MOVES, because at wide
     viewports the CLAMP CEILING binds and the vw term never applies. At 1998px:
     18vw was 300px, 8vw was 150px, 12vw is 200px — all three pinned to their
     ceilings. So "18 to 8" was really 300 to 150. Floor and ceiling are kept
     proportional (the same vw evaluated at 778px and 1667px, the widths where
     the original's own bounds took over), so the shape of the response across
     viewports stays what it was.
     🔴 THE REASON IT CAN COME BACK IN IS THE WIDER CONTAINER, NOT A CHANGE OF
     MIND ABOUT THE WEDGE. The 18vw above was measured against a 1240px
     container, where the copy column ended much further left and the wall's
     ragged corner sat right against it. The hero's container is 1666px now, so
     the copy already reaches further right and the wall no longer needs to be
     pushed as far to clear it. At 1998px this is 160px where it used to be 300.
     ⚠️ THE WEDGE IS THE THING TO WATCH, and it is a LOOKING test, not a
     measuring one: the failure is the empty triangle reading as a NOTCH bitten
     out of the wall rather than as an even margin. If it returns, put this
     back up rather than reaching for the tilt or the tile count — both were
     tried against this same symptom and both were wrong.

     🔴 CURRENT STATE, SO NOBODY READS THE HISTORY ABOVE AS THE RULE:
     from 900px up this margin is ZERO. The whole separation lives in
     `--gal-wall-inset`, which is part of the COPY's grid track (see
     .wsite-gal__layout) — that is what lets the copy centre in the space
     instead of being pinned to its left edge, without the wall moving.
     The clamp in `--gal-wall-margin` is the UNDER-900 value only, where the
     hero stacks and there is no copy column beside the wall to give it up. */
  margin-left: var(--gal-wall-margin);

  /* Transform origin is set below, with the reasoning for both axes. */
  /* ⏹ CENTRE, AS THE MOCKUP -- the top-left anchor is GONE. It was introduced
     to stop the scaled wall sliding into the copy and to stop the wall
     floating as a band in the middle of the pane. Both of those were symptoms
     of the flattened 3D chain: with real perspective the wall recedes rather
     than sliding, and it fills the pane by depth rather than by stacking more
     tiles. Anchoring at the top-left with true 3D throws the near edge of the
     wall at the camera -- measured: tiles 12,670px tall, 40,000px off-screen. */
  transform-origin: 50% 50%;
  transform-style: preserve-3d;

  /* 🔴 DEFAULTS ARE THE AT-REST END STATE, NOT THE START STATE. If the script
     never runs — or rAF never fires, which is exactly what stranded the steps
     line — the wall sits upright and readable rather than folded flat on its
     face at 75 degrees. Nothing here can leave the hero looking broken by
     failing to run. */
  transform: rotateX(var(--gal-rx, 0deg)) scale(var(--gal-sc, 1));
  will-change: transform;
}

.wsite-gal__col {
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* 🔴 THE QUERY CONTAINER FOR `.wr`. The tiles size their internals from
     `cqw`, and a tile cannot be its own container — without this declaration
     the unit falls back to the VIEWPORT and every render draws ~4x too large.
     This column's inline size IS the tile width, so it is the right container.
     See the note at the top of site-renders.css. */
  container-type: inline-size;
  transform: translateY(var(--gal-y, 0%));
  will-change: transform;
}
/* The middle lane starts higher, as the original offsets it. */
.wsite-gal__col:nth-child(2) { margin-top: -28%; }
.wsite-gal__col .wr { flex: 0 0 auto; }

/* ---- the copy, and the scrim that makes it legible ---------------------- */

/* The copy centres itself inside its own column, now that the column stretches
   to the full height of the pane. */
.wsite-gal__copy {
  position: relative;
  z-index: 3;
  display: grid;
  align-content: center;
}

/* The copy measure. The white panel behind it no longer needs to be told this
   number — it is drawn from the copy element's own box (see ::before below) —
   so this is now just the measure, declared once. */
/* 🔴 40rem, AND THE HEADLINE IS SIZED BY VIEWPORT HEIGHT — THE SECOND HALF OF
   THIS NOTE IS BECAUSE THE COPY WAS BEING CLIPPED, MEASURED AT 856px INSIDE A
   632px PANE.

   ⚠️ THIS NUMBER HAS DRIFTED FROM ITS OWN COMMENT BEFORE: the heading said
   34rem while the declaration said 32rem. It is 40rem now — if you change the
   value, change this line with it.

   ⏹ AND BACK UP TO 36rem — THE FLOOR THAT PUTS BOTH BUTTONS ON ONE LINE.
   Brees, 8/28: "it would look better if both buttons were on the same line."
   The two buttons plus their gap need ~567px; at 33rem (528px) the flex row
   wrapped and they stacked. Swept 33/36/37rem: 33 stacks, 36 is inline, and the
   headline holds four lines at all three. So 36rem is the minimum that buys the
   inline row — the block is 48px wider than the "tightest neat" setting below,
   and that is the price of the button row, paid deliberately.
   📌 The two rules below still hold and are why 36 is safe: wider never breaks
   the four-line headline (only narrower does), and the body is capped at 34ch
   regardless. Only the button row cares about this number going UP.

   ⏹ 40rem CAME DOWN TO 33rem, AND 33 IS A MEASURED FLOOR, NOT A PREFERENCE.
   Brees, 8/28: the copy "feels a little stretched versus nice and neat."
   Swept 32-40rem at four viewports: at 1600px and up the headline re-breaks to
   FIVE lines at 32rem, so 33rem is the tightest setting that still holds the
   four-line break he approved. It is also almost exactly the headline's own
   PAINTED width — 527px of text in a 528px track — which is what removes the
   stretch: at 40rem the block carried 113px of dead track to the right of the
   longest line, and the ragged right edge is what read as loose.
   ⚠️ DO NOT DROP TO 32rem TO TIGHTEN IT FURTHER. That is the exact value where
   the headline breaks, and it breaks only at ≥1600px — a narrow-viewport check
   would pass it.

   The original reasoning for going wide, still true and still the reason the
   floor is where it is:
   40rem (was 32rem) so the headline sets on FOUR lines with "shouldn't be one."
   intact, as in the reference. 🔴 WIDER IS THE SAFE DIRECTION FOR THE COPY: the
   pane clips on HEIGHT, and a wider measure takes a line OUT of the headline
   rather than adding one. It does not lengthen the body copy either —
   `.wsite-gal__sub` caps itself at 34ch, well inside this.

   ⚠️ BUT IT IS NOT FREE, AND THE COST LANDS ON THE WALL. These are two tracks
   of one grid, so every pixel the measure takes comes off the wall's column.
   Measured at 1024px wide: a flat 40rem left the wall 238px, down from 366px —
   the wall went thin and stopped reading as a wall at all. `min()` caps the
   measure at HALF the viewport, so it is the full 40rem at every width from
   1280 up (where the reference lives) and lands back on exactly the old 32rem
   at 1024. Nothing narrower than 1280 is worse off than before this change.
   📌 The 1998px reference width is not the only width this has to work at;
   it looked right there while quietly costing 130px at 1024.

   The pane is `100vh - --hdr-h-min` and carries `overflow: hidden` for the
   wall, so anything taller than it is simply cut — and the first line of the
   headline was, silently. At 30rem and the global 72px display size the
   headline alone ran to SIX lines and 458px.

   ⚠️ THE CONSTRAINT HERE IS HEIGHT, NOT WIDTH, so the type scales with `vh`.
   Every other headline on this site scales with width, which is right for a
   headline in normal page flow; this one lives inside a fixed-height pinned
   pane, and a viewport that is wide but SHORT is the case that breaks it.
   📌 Short viewports are the normal case, not an edge case — this project
   already recorded it on a previous build, where Windows display scaling makes
   1536x700 a standard desktop and a hero overlap bug lived on viewport height
   for weeks while width-only checks passed.

   The clamp floor keeps it readable on a phone; the ceiling stops it
   overtaking the global display size on a tall monitor. */
.wsite-gal__layout,
.wsite-gal__copy { --gal-copy-w: min(36rem, 50vw); }

.wsite-gal__copy-inner { max-width: var(--gal-copy-w); }

/* THE COPY BLOCK CENTRES IN ITS COLUMN — the two-column layout only, so the
   handoff from the wall's inset to the copy's track happens at exactly the
   width where the two columns exist.
   🔴 SCOPED TO 900px UP FOR A REASON. Below that the hero stacks: the copy is
   static, full width, and sits ABOVE the wall rather than beside it. Centring
   a block there would put a short paragraph in the middle of a phone screen
   with ragged margins either side, and zeroing the wall's margin would drag
   the wall left at the one width where it has no copy beside it to clear.
   Both stay as they were under 900px.
   ⚠️ The text itself stays LEFT-ALIGNED — Brees' call, 8/28, choosing the
   block-centred option over centring the type. The headline, paragraph and
   both buttons keep one shared left edge. */
@media (min-width: 900px) {
  .wsite-gal__layout {
    --gal-wall-inset: clamp(150px, calc(55vw - 550px), 390px);
    --gal-wall-margin: 0px;
  }

  /* 🔴 CENTRED AGAINST THE WALL'S *PAINTED* EDGE, NOT ITS LAYOUT BOX — AND THE
     TWO ARE 99-215px APART.

     The first attempt centred against the box and Brees came back with two
     notes: the copy was still too far right, and the wall was "cut off". Same
     cause. At rest the grid is scaled 1.2 AND raked 75deg, so the near tiles
     project a long way outside the grid's box; the artwork a reader sees
     starts well left of where the layout says the column begins. Centring on
     the box therefore centres on a line the artwork does not respect, and the
     copy's white panel — sized from the copy — was left overlapping the first
     column of tiles, which is the "cut off" edge he circled.

     🔴 THE OVERHANG CANNOT BE MODELLED, ONLY MEASURED. A scale of 1.2 predicts
     0.1 x grid width = 64px at 1440; the real figure is 111px, because
     perspective projects the near tiles outward on top of the scale, and
     non-uniformly. Measured by hiding the copy and scanning the render for its
     leftmost painted pixel (scripts/wall-painted-edge.js + painted-edge-scan.py):

         viewport   wall box   painted   overhang
         1998           1002       787      215px
         1728            866       684      182px
         1600            824       676      148px
         1440            801       690      111px
         1366            790       691       99px
         1280            778       675      103px
         1024            712       610      102px

     It is not a constant, not a constant fraction of the grid (0.050-0.078),
     and not a constant vw (7.25-10.76) — it turns over because the container
     stops growing at 1666px while the wall keeps widening. So this padding is
     a CLAMP FITTED TO THOSE MEASUREMENTS, not a derivation: it lands the block
     within ~15px of true centre from 1024 to 1998, against the 50-108px error
     it had when centred on the box.
     ⚠️ RE-MEASURE, DO NOT RE-REASON, if the wall's scale, tilt, tile count or
     the container cap ever change. Every one of those moves the painted edge,
     and none of them moves the layout box. */
  .wsite-gal__copy {
    justify-items: center;
    padding-right: clamp(68px, 7.5vw, 176px);
  }
}

/* [!] MATCHED TO THE OTHER FOUR PAGES - BREES, 9/9: "make sure that all the
   Section 1 H1s are the same size. Right now the Websites page seems to be
   larger." Measured at 1440x900 it was 64.8px against 46.08px everywhere else,
   +41%.

   [!] THE CAUSE WAS THE UNIT, NOT THE NUMBER. Every other hero H1 runs
   `.hero__headline` at clamp(2rem, 3.2vw, 3.25rem) - sized on WIDTH. This one
   was sized on HEIGHT (7.2vh), so the two did not merely differ by a constant,
   they diverged differently at every viewport and could never be "the same
   size" at more than one. Adopting the shared expression verbatim is what
   makes them track together, so this cannot drift apart again.
   [i] Not switched to the class itself: this headline lives in the 3D gallery
   hero and keeps its own line-height and margin.

   [!] RE-MEASURE THE WALL AFTER TOUCHING THIS. websites.css records that the
   painted headline is 502px and that the 8/28 balance pass tuned the copy
   column against that exact number. A smaller headline changes it. */
.wsite-gal__headline {
  margin: 0 0 var(--s-3);
  font-size: clamp(2rem, 3.2vw, 3.25rem);
  line-height: 1.04;
}

/* 🔴 44ch, AND THE CONTAINER WAS NEVER THE THING THAT WAS NARROW.
   Brees, 8/28, once the buttons went inline: "widen the text and paragraph
   container to make it a little more balanced."

   The paragraph carries its OWN cap, independent of `--gal-copy-w`, so
   widening the copy container would not have moved this by a pixel — it would
   have added slack to the right of an unchanged paragraph, which is the
   opposite of balanced. Swept measure 36-42rem against sub caps 34-48ch and
   measured the three PAINTED widths a reader actually compares:

       sub cap   headline   paragraph   buttons   spread
       34ch         502        415        567      152px
       40ch         502        494        567       73px
       44ch         502        544        567       65px
       48ch         502        594        567       92px

   The headline (502) and the button row (567) do not move at ANY measure from
   36 to 42rem — the line breaks and the buttons are what they are. So the
   container stays at 36rem, which is already snug around the 567px button row,
   and the paragraph alone opens up to meet it. 44ch is the minimum spread.
   📌 Also the more readable setting: 34ch was well under the comfortable range
   for body text, and this change costs nothing in height — a wider measure
   takes lines OUT of the paragraph, and this pane clips on height. */
.wsite-gal__sub {
  margin: 0 0 var(--s-4);
  max-width: 44ch;
  font-size: clamp(1rem, 2.4vh, 1.25rem);
}

/* 🔴 THE SCRIM IS A CONTRAST DEVICE. Without it the headline's contrast
   depends on which render happens to drift behind it, which is not a contrast
   anyone can measure once. On solid white, --ink is 13.30:1.

   ⏹ IT USED PERCENTAGE STOPS IN THE MOCKUP AND THAT WAS A REAL FAILURE: at
   34/46/62% it was fully opaque only to x=497 on a 1537px viewport while the
   headline ran to x=568 — ~70px of display type over the FADE, measuring
   1.64:1 against an indigo render behind it. Percentages of the VIEWPORT
   cannot track a copy block sized in rem; they drift apart at every width and
   happened to look right at the one width it was built at.

   The panel's width is now the copy's own right edge + 56px, so it is correct
   by construction at every viewport.
   ⚠️ Same family as the `100%`-inside-a-custom-property and `22ch`-on-a-
   wrapper bugs already recorded here: a relative unit resolved against the
   wrong thing, producing a legal value no gate fails on.

   🔴 SOLID, WITH A HARD EDGE. The mockup faded white into transparent
   across the wall; integrating it brought that fade in, and verify.js failed
   this sheet's `no gradients` check on the first run afterwards. Solid colours
   only is the standing house rule here. **Do not reintroduce the fade.**
   It is also the better answer on its own terms: across a fade, the text
   nearest the edge sits on a ground that is neither white nor a render, so its
   contrast is a RANGE rather than a number. A solid panel makes it 13.30:1.
   📌 Do not name the forbidden function in prose in this file either — the
   gate is a raw regex over the whole sheet. */
/* 🔴 THE PANEL IS DRAWN FROM THE COPY'S OWN BOX, NOT RECOMPUTED FROM THE
   VIEWPORT — and the arithmetic version was wrong twice before this.

   Attempt 1 used percentage stops. Attempt 2 used
   `max(--s-4, 50vw - --container-w/2) + --gal-copy-w + 56px`, which measured
   648px against a headline whose right edge was ALSO 648px — a one-pixel
   margin, and only by coincidence: the +56px happened to equal `.container`'s
   64px padding, which the formula never accounted for, while `50vw` overshot
   the true gutter because **vw includes the scrollbar** (1463 against a 1448px
   clientWidth). Two errors cancelling into something that looked deliberate.

   Anchoring to the copy element removes all of it: `right: -64px` is 64px past
   the real text edge at every width, whatever the container padding or the
   scrollbar does. There is nothing left to keep in sync.

   `left: -100vw` / `top`+`bottom: -100vh` simply guarantee the panel reaches
   the pane's edges; the sticky pane's `overflow: hidden` clips it.
   ⚠️ `z-index: -1` keeps it behind the copy but still inside `.wsite-gal__copy`
   (z-index 3), so it paints ABOVE the wall (z-index 1) and BELOW the text. */
.wsite-gal__copy-inner { position: relative; }

.wsite-gal__copy-inner::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: -100vw;
  /* ⏹ WAS `calc(var(--s-4) * -1)` — 40px PAST the copy, to land in the middle
     of the layout gutter. That was correct while the copy was pinned to the
     left of its track with the gutter immediately beyond it. Once the block
     centred, the panel's right edge moved right with it and those 40px pushed
     solid white over the first column of tiles — the "cut off" graphic.
     🔴 THE PANEL EXISTS TO PUT SOLID WHITE BEHIND THE COPY, AND THE COPY ENDS
     AT THIS BOX. Anything past it is white painted over artwork. The headline
     fills the measure, so stopping here still covers every glyph: --ink stays
     13.30:1, which is the whole point of the panel.
     ⚠️ Do not reintroduce a positive overshoot here to "give it some air".
     There is nothing to the right but the wall. */
  right: 0;
  top: -100vh;
  bottom: -100vh;
  background-color: var(--white);
}

@media (max-width: 900px) {
  /* 🔴 STACKED, NOT OVERLAID. A solid panel wide enough to protect the copy
     on a phone would cover the wall completely, and a translucent one puts the
     contrast back into a range. So the two simply stop overlapping: copy
     first, wall beneath it, panel switched off. The sticky track collapses
     with them — 240vh of pinned scroll is a desktop gesture. */
  .wsite-gal { height: auto; }
  .wsite-gal__sticky { position: static; height: auto; padding-block: var(--s-5); }
  .wsite-gal__copy-inner::before { display: none; }
  .wsite-gal__copy { position: static; padding-bottom: var(--s-5); }
  .wsite-gal__grid {
    grid-template-columns: repeat(2, 1fr);
    height: 62vh;
    transform: none !important;
  }
  .wsite-gal__col { transform: none !important; }
  .wsite-gal__col:nth-child(2) { margin-top: 0; }
  .wsite-gal__col:nth-child(3) { display: none; }
}

/* 🔴 REDUCED MOTION KILLS THE MECHANIC, NOT JUST THE EASING — 2.3.3.
   A 75deg scroll-driven rotation is precisely what that criterion is about.
   The track also has to COLLAPSE: leaving 240vh of empty scroll with a static
   wall in it would strand a reader who asked for less motion in more scrolling
   than everyone else. The hero becomes an ordinary stacked block. */
@media (prefers-reduced-motion: reduce) {
  .wsite-gal { height: auto; }
  .wsite-gal__sticky {
    position: static;
    height: auto;
    padding-block: var(--s-6);
  }
  .wsite-gal__grid { transform: none !important; }
  .wsite-gal__col { transform: none !important; }
  .wsite-gal__col:nth-child(2) { margin-top: 0; }
  .wsite-gal__copy { position: static; padding-bottom: var(--s-5); }
  .wsite-gal__copy-inner::before { display: none; }
}


/* ==========================================================================
   RESPONSIVE PASS (9/16) — WEBSITES, BELOW 900

   🔴 THE HERO NEVER STACKED. The under-900 block (above) collapses the sticky
   track and the wall, but not the LAYOUT GRID: `.wsite-gal__layout` kept its
   two tracks, and the copy track is `min(36rem, 50vw)` — 195px on a 390px
   phone. The headline set one word per line in the left half of the screen
   while the wall showed as a 60px sliver on the right. One column here; the
   wall sits under the copy as the stacked note always intended.
   ========================================================================== */
@media (max-width: 899px) {
  .wsite-gal__layout {
    --gal-copy-w: 40rem;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto;
    height: auto;
    transform: none;
  }

  .wsite-gal__copy { --gal-copy-w: 40rem; }
  .wsite-gal__copy-inner { max-width: 40rem; }

  .wsite-gal__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    height: min(62vh, 560px);
    margin-left: 0;
    margin-right: calc(-1 * var(--container-pad));
  }
}

@media (max-width: 599px) {
  /* Both hero buttons full width and one line each. At 50vw they were 195px
     boxes with two-line labels. */
  .wsite-hero__actions { display: grid; gap: var(--s-2); }
  .wsite-hero__actions .btn { text-align: center; }

  /* The build/subscription table. Kept as the two-column table on purpose —
     the pairing across the gutter is the section's argument, and the note
     above records why a stacked version was removed. It simply did not FIT:
     the price is nowrap at 28px and the gutter is 64px, so "The Subscription"
     was cut mid-word at the screen edge. Tighter gutter, smaller figures,
     type one step down — both columns now fit a 320px phone with no scroll. */
  .wsite-table { --wcol-gap: 16px; font-size: 0.9375rem; }
  .wsite-table__price { font-size: 1.375rem; white-space: normal; }
  .wsite-table__unit { font-size: 0.875rem; }
  .wsite-pill { padding: 5px 10px; font-size: 0.75rem; letter-spacing: 0.03em; }

  /* "See the work: Browse our portfolio →" wrapped its arrow onto a line of
     its own inside the pill. */
  .wproof__cta .btn { display: block; padding-inline: 16px; text-align: center; }
}
@media (max-width: 374px) {
  .wproof__cta .btn { font-size: 0.875rem; padding-inline: 12px; }
}

/* ==========================================================================
   9/16 (later) — HERO: REAL SCREENSHOTS + AUTOPLAY
   1. The wall's tiles are the Portfolio page's screenshots (hero-gallery.js builds .wr--shot): browser chrome on
      top, the capture filling the rest at the tiles' own 16:10 frame, top-anchored so each site's header shows.
   2. The unfurl and the hand-off to section 2 now PLAY (hero-gallery.js, render-handoff.js) instead of being
      scrubbed across a 190vh pinned track, so the track collapses to the hero itself: no dead scroll.
      ⚠️ The phase-split / track-height pairing recorded above no longer applies — time drives P now.
   ========================================================================== */
.wr--shot { display: flex; flex-direction: column; }
.wr--shot .wr__shot { display: block; width: 100%; height: auto; aspect-ratio: 1440 / 900; object-fit: cover; object-position: top center; }
.wsite-gal { height: auto; }
@media (min-width: 901px) {
  .wsite-gal__sticky { position: relative; top: auto; }
}

/* 9/16 (later) — THE WESTERVILLE QUOTE LOSES ITS LINE. Brees: "I don't like using the border line to mark the
   quote"; picked the big lime quote mark. An oversized opening mark in the display face sits above the quote,
   decorative (the quote is already a <blockquote> with its own curly quotes), so it is a pseudo-element and never
   read aloud. Lime on indigo 5.44. */
.wproof__quote {
  position: relative;
  padding-left: 0;
  padding-top: clamp(3.5rem, 5.4vw, 5rem);
  border-left: 0;
}
.wproof__quote::before {
  content: "\201C";
  position: absolute;
  left: -0.04em;
  top: 0;
  font-family: var(--font-display);
  font-size: clamp(9rem, 13vw, 12.5rem);
  line-height: 0.72;
  letter-spacing: -0.04em;
  color: var(--lime);
  pointer-events: none;
}

/* ==========================================================================
   9/16 (later) — THE DIGITAL EMPATHY APPROACH AS TABS (concept 8B)
   Left: the heading, then three pill tabs (numeral icon + title). Right: a white reading panel holding the chosen
   one's numeral, title and body. Replaces the accordion + the full-bleed aside panel, so this variant carries
   ordinary section padding again (the zero-padding rules above belong to the old aside and are out-specified here).
   Contrast: white on indigo 8.09 · ink on lime 8.95 · ink on white 13.3.
   ========================================================================== */
.page-wsite .wsite-approach.wsite-approach--tabs { min-height: 0; padding-top: var(--section-pad); padding-bottom: calc(var(--section-pad) + var(--corner)); }
.wtabs { display: grid; gap: var(--s-5); }
@media (min-width: 900px) {
  .wtabs { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); column-gap: var(--s-6); align-items: stretch; }
}
.wsite-approach--tabs .wsite-approach__head { margin: 0 0 var(--s-5); color: var(--white); max-width: 12ch; }
.wtabs__list { display: grid; gap: 12px; }
.wtabs__tab {
  display: flex;
  align-items: center;
  gap: 18px;
  width: 100%;
  min-height: 72px;
  padding: 14px 26px 14px 22px;
  border: 0;
  border-radius: var(--r-pill);
  background-color: rgb(243 238 232 / 0.08);
  color: var(--white);
  font: inherit;
  font-size: 1.1875rem;
  font-weight: 500;
  line-height: 1.3;
  text-align: left;
  cursor: pointer;
  transition: background-color 0.3s var(--ease-out), color 0.3s var(--ease-out);
}
.wtabs__tab:hover { background-color: rgb(243 238 232 / 0.16); }
.wtabs__tab:focus-visible { outline: 3px solid var(--lime); outline-offset: 3px; }
.wtabs__tab[aria-selected="true"] { background-color: var(--lime); color: var(--ink); }
/* 🔁 9/17, Alie (review note 987597): "Can we replace the numbers with icons?" The num-*.png pair is gone from
   this device — small icon in the tab, glyph-on-disc in the panel, drawn in the homepage product-card style
   (24x24, fill none, currentColor, stroke 1.6, round caps/joins) so the site has ONE icon language.
   🟢 A REAL FIX FELL OUT OF IT. The tab numeral was a coloured PNG, and on the SELECTED tab that is lime on lime
   — the "1" all but vanished, on this page and on the homepage copy of the device. The icon is `currentColor`,
   so it flips with the label: white on indigo when resting, ink on lime when chosen. Legible in both states by
   construction rather than by luck.
   ⚠️ The panel disc colours rotate petal · lime · sky, matching the homepage cards' rotation. Adding a fourth
   tab means continuing that run, not restarting it. Glyph is ink on every disc: petal 7.80 · lime 8.95 · sky 9.83. */
.wtabs__tab-ic { flex: none; display: block; width: 26px; height: 26px; }
.wtabs__tab-ic svg { display: block; width: 100%; height: 100%; }
.wtabs__ic {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: flex-start;
  width: clamp(48px, 4.2vw, 60px);
  aspect-ratio: 1;
  border-radius: var(--r-pill);
  background-color: var(--disc, var(--lime));
  color: var(--ink);
}
.wtabs__ic svg { width: 56%; height: 56%; }

.wtabs__panels { display: grid; }
.wtabs__panel {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--s-4);
  padding: clamp(32px, 4.5vw, 64px);
  border-radius: 40px;
  background-color: var(--white);
  color: var(--ink);
}
.wtabs__panel:focus-visible { outline: 3px solid var(--lime); outline-offset: 4px; }
/* one cell, the unchosen hidden but kept for height, so the panel never jumps between tabs */
.wtabs.is-armed .wtabs__panel[hidden] { display: flex; visibility: hidden; }
.wtabs:not(.is-armed) .wtabs__panels { gap: var(--s-3); }
.wtabs:not(.is-armed) .wtabs__panel { grid-area: auto; }
.wtabs:not(.is-armed) .wtabs__panel[hidden] { display: flex; }
/* ⏹ `.wtabs__num` retired with the numerals (987597) — the panel now uses `.wtabs__ic`. */
.wtabs__title { margin: 0; font-family: var(--font-display); font-weight: 500; font-size: clamp(1.75rem, 3vw, 2.75rem); line-height: 1.1; }
.wtabs__body { margin: 0; max-width: 52ch; font-size: clamp(1.125rem, 1.5vw, 1.3125rem); line-height: 1.65; }
/* the inline link is lime on indigo elsewhere; on the white panel it is ink, still underlined */
.wtabs__body .wsite-inline-link { color: var(--ink); }
@keyframes wtabs-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.wtabs__panel.is-lit > * { animation: wtabs-in 0.45s var(--ease-out) both; }
.wtabs__panel.is-lit > :nth-child(2) { animation-delay: 70ms; }
.wtabs__panel.is-lit > :nth-child(3) { animation-delay: 140ms; }
@media (max-width: 899px) {
  .wsite-approach--tabs .wsite-approach__head { margin-bottom: var(--s-4); }
  .wtabs__panel { border-radius: 28px; }
}
@media (prefers-reduced-motion: reduce) {
  .wtabs__tab { transition: none; }
  .wtabs__panel.is-lit > * { animation: none; }
}

/* 🔁 9/16 (later), Brees: "Digital Empathy Approach section - icons need to be transparent. Also the text in this
   section feels very very large. Card titles need to be altivo."
   - Tab icons use num-*-t.png (white ground flood-filled out from the edges; the panel keeps the originals, on white).
   - Panel title: All Round Gothic 28-44px -> ALTIVO 500 at 22-28px (the tab labels were already Altivo).
   - Panel body 18-21px -> 16-18px; tab labels 19px -> 17px; panel numeral and padding scaled down with them. */
.wtabs__tab { min-height: 64px; font-size: 1.0625rem; }
.wtabs__tab-ic { width: 24px; height: 24px; }
.wtabs__panel { gap: var(--s-3); padding: clamp(28px, 3.4vw, 48px); }

.wtabs__title { font-family: var(--font-body); font-weight: 500; font-size: clamp(1.375rem, 1.9vw, 1.75rem); line-height: 1.25; }
.wtabs__body { font-size: clamp(1rem, 1.15vw, 1.125rem); line-height: 1.7; }

/* ==========================================================================
   9/17 (review note 987599) — COSTS AS TWO CARDS, on the SEO page's idiom
   Replaces the side-by-side <table> and its scroll region.
   🔴 A HAIRLINE, NOT A SHADOW — the same call `.costs__card` records: this page spends its one permitted
   elevation on the arch panels, and two raised slabs here would flatten that distinction.
   🔴 `margin-top: auto` on nothing here BY DESIGN: both lists are six items long, so the cards match without it.
   If a list ever changes length, add it the way `.costs__card` does or the cards end ragged.
   Contrast: ink on white 13.3 · ink-muted on white above 5 · ink on petal 7.80 · ink on lime 8.95.
   ========================================================================== */
.wsite-costs__cards {
  display: grid;
  gap: var(--s-3);
  margin: var(--s-5) 0 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 860px) { .wsite-costs__cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.wsite-ccard {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: var(--s-4);
  border: 1px solid var(--ink-10);
  border-radius: var(--r-render);
  background-color: var(--white);
}
.wsite-ccard__price {
  margin: var(--s-3) 0 0;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(2.25rem, 4vw, 3rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--ink);
}
/* the unit rides at the price's own baseline, a third of its size — the SEO card's proportion */
.wsite-ccard__unit { font-family: var(--font-body); font-size: 0.34em; font-weight: 400; letter-spacing: 0; color: var(--ink-muted); }
.wsite-ccard__what { margin: var(--s-2) 0 var(--s-3); font-size: var(--fs-body); line-height: 1.5; color: var(--ink); }
.wsite-ccard__list { margin: 0; padding-top: var(--s-3); border-top: 1px solid var(--ink-10); list-style: none; font-size: var(--fs-small); line-height: 1.5; }
.wsite-ccard__list li { position: relative; margin-bottom: 8px; padding-left: 16px; color: var(--ink); }
.wsite-ccard__list li:last-child { margin-bottom: 0; }
.wsite-ccard__list li::before { content: "·"; position: absolute; left: 4px; top: -1px; font-weight: 700; color: var(--ink-muted); }

/* ==========================================================================
   9/16 (later) — "NEED MORE WORDS?" AS A LIME PILL BANNER (concept 9B)
   Same three elements (heading, sentence, link), restyled: one lime pill, the heading in the display face, the link
   as an ink pill button. Stacks into a rounded lime card below 1100 where one row cannot hold all three.
   Contrast: ink on lime 8.95 · white on ink 13.3.
   ========================================================================== */
.wsite-more {
  /* 🔁 9/18, Brees: "Need more words" was "flush up against the boxes above it" — measured 20px under the two cost
     cards at every width. The same lime callout sits 116px under its cards on Launching and Technology
     (`.lp-more { margin-top: var(--s-6) }`), so it takes the same token here and the three pages share one rhythm. */
  margin-top: var(--s-6);
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--s-3) var(--s-5);
  padding: 22px 22px 22px 44px;
  border-radius: var(--r-pill);
  background: var(--lime);
}
.wsite-more__head { font-family: var(--font-display); font-weight: 500; font-size: clamp(1.75rem, 2.6vw, 2.5rem); line-height: 1.05; white-space: nowrap; }
.wsite-more__body { font-size: 1.125rem; line-height: 1.5; }
.wsite-costs .wsite-more .wsite-more__link {
  justify-self: end;
  display: inline-flex;
  align-items: center;
  min-height: 56px;
  padding: 0 26px;
  border-radius: var(--r-pill);
  background-color: var(--ink);
  color: var(--white);
  font-weight: 700;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
}
.wsite-costs .wsite-more .wsite-more__link:hover { background-color: #232328; color: var(--white); }
.wsite-costs .wsite-more .wsite-more__link:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
@media (max-width: 1099px) {
  .wsite-more { grid-template-columns: minmax(0, 1fr); padding: var(--s-4); border-radius: 32px; }
  .wsite-more__head { white-space: normal; }
  .wsite-costs .wsite-more .wsite-more__link { justify-self: start; white-space: normal; }
}

/* 9/16 fixes after looking: (1) the tabs heading inherited the old aside's right alignment; (2) in one row the lime
   banner squeezed the sentence into ~260px between the nowrap heading and the nowrap button, so the heading and the
   sentence now stack on the left with the button beside them. */
.wsite-approach--tabs .wsite-approach__head { text-align: left; margin-left: 0; padding: 0; }
@media (min-width: 1100px) {
  .wsite-more {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "head link" "body link";
    row-gap: 6px;
    padding: 30px 30px 30px 48px;
  }
  .wsite-more__head { grid-area: head; }
  .wsite-more__body { grid-area: body; }
  .wsite-costs .wsite-more .wsite-more__link { grid-area: link; align-self: center; }
}

/* ==========================================================================
   9/17 — BREES' WEBSITES EDITS
   ========================================================================== */
/* 1. The landed render is bigger. Brees: "When the [Sonoran] Sky Pet Hospital mockup comes down from section 1 into
   section 2, it shrinks. I would like it bigger." The slot was a fixed 26rem (416px) column; it is now the larger
   share of the grid (~577px at 1440, +39%). render-handoff.js re-reads the slot rect every frame, so the flier simply
   lands at the new size; `.wr` scales from the slot's width (container query), so nothing inside distorts. */
@media (min-width: 900px) {
  .wsite-problem__grid { grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); row-gap: var(--s-4); }
  /* 🔁 Brees: "now the text on the left is incredibly tall compared to the screenshot." The heading was squeezed into
     the narrower column and wrapped to five lines. It now runs ACROSS both columns (the copy wrapper steps out of the
     grid with display: contents), and only the two paragraphs sit beside the screenshot, vertically centred on it.
     Source order is unchanged: heading, paragraphs, slot. */
  .wsite-problem__copy { display: contents; }
  .wsite-problem__head { grid-column: 1 / -1; grid-row: 1; max-width: 30ch; margin-bottom: 0; }
  .wsite-problem__body { grid-column: 1; grid-row: 2; align-self: center; }
  .wsite-problem__media { grid-column: 2; grid-row: 2; }
}

/* 2. "The Digital Empathy Approach" matches every other section heading on the page (How it works etc.: the shared
   .t-h2 size, 48px at 1440 / 37px at 1024). It had kept the old aside's 66px override. */
.wsite-approach--tabs .wsite-approach__head { font-size: var(--fs-h2); max-width: none; }

/* 3. FLIPPED: the white reading panel on the LEFT, the title and tabs on the RIGHT. Brees: "flip this section so the
   white box would be on the left and the title and buttons would be on the right."
   🔴 Placed with grid-column, NOT by moving markup — the source (and so the reading and Tab order) stays
   title -> tabs -> panel, which is what the WAI-ARIA tabs pattern expects. Stacked under 900 nothing moves. */
@media (min-width: 900px) {
  .wtabs { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
  .wtabs__aside  { grid-column: 2; grid-row: 1; }
  .wtabs__panels { grid-column: 1; grid-row: 1; }
}
