/* /assets/css/site.css — the one stylesheet every presive.co page shares (b105; b106).
 *
 * Values come from /assets/css/tokens.css (linked first by the load-order block, b106 S01); this file holds the
 * components and global rules that must be identical on every page. It loads after each page's own <style>
 * (motion-includes partial), so an equal-specificity rule here wins by order.
 * The --pv-* names are b105's; they now alias tokens.
 */
:root{
  --pv-consent-z:88;                        /* above the float pill (80), below menu (90) / sheets (95) / skip (200) */
  --pv-consent-w:420px;                     /* b106 D5: the card's width below 1024 px; from 1024 px (b107) it is one row, at most 940 px */
  --pv-consent-bg:var(--panel-2);                 /* b107 N11: opaque; captions showed through at 96 % */
  --pv-consent-shadow:0 18px 50px -18px color-mix(in srgb,var(--bg-darker) 70%,transparent);
  --pv-gutter:var(--gutter);
}

/* ============================================================
   CONSENT BANNER (b105 step 02; b106 S02 / D5)
   ≥ 1024 px (b107): one row, at most 940 px wide, at the bottom left on every page (the b106 top placement on the
   home page went with the ribbon field; the html.pv-consent-top rules below are kept for a page that needs them).
   Below 1024 px: a short sheet across the bottom: one line, two buttons, the close button.
   While it is open, cookies.js publishes its height as --pv-banner-h and sets html.pv-consent-open; the
   scroll-padding and bottom room below keep every focused control clear of it (WCAG 2.4.11). cookies.js also sets
   html.pv-consent-top on the home page (classes, not :has() on <html>: those re-match on every DOM change).
============================================================ */
.pv-consent{
  position:fixed;z-index:var(--pv-consent-z);
  left:var(--pv-gutter);bottom:max(var(--pv-gutter),env(safe-area-inset-bottom));
  width:min(var(--pv-consent-w),calc(100% - 2 * var(--pv-gutter)));
  color:var(--white);
  background:var(--pv-consent-bg);
  border:1px solid var(--line-strong);
  border-radius:var(--radius-card);
  box-shadow:var(--pv-consent-shadow);
  font-family:inherit;
}
.pv-consent[hidden]{display:none}
/* b108 A07/A10: forced colours drop background-only shapes. A border costs nothing in normal mode and gives
   every button, the consent card and the wizard's progress bar an outline that high-contrast users can see. */
@media (forced-colors:active){
  .btn,.pv-consent__btn,button{border:1px solid ButtonText}
  .pv-consent{border:1px solid CanvasText}
  .q-bar,.q-bar__fill,progress{border:1px solid CanvasText;forced-color-adjust:none}
}
/* b108 N01: below 1024 px the sheet is the full width of the screen's foot, so anything at the bottom of a
   short page (the quote wizard's Next, most of all) sat underneath it on a first visit. While the sheet is
   up the page reserves its height, so every control can be scrolled clear. cookies.js publishes the height. */
@media (max-width:1023px){
  html.pv-consent-open body{padding-bottom:calc(var(--pv-banner-h,140px) + 16px)}
}
.pv-consent__in{position:relative;display:flex;flex-direction:column;gap:14px;padding:18px 18px 18px 20px}
.pv-consent__copy{min-width:0;padding-right:36px}
.pv-consent__title{
  font-size:var(--fs-micro);font-weight:600;letter-spacing:var(--ls-caps);
  text-transform:uppercase;color:var(--foot-cat);line-height:1.2;
}
.pv-consent__txt{margin-top:6px;font-size:var(--fs-xs);line-height:1.5;color:var(--gray-3);text-wrap:pretty}
.pv-consent__txt a{color:var(--white);text-decoration:underline;text-underline-offset:3px;text-decoration-thickness:1px;white-space:nowrap}
.pv-consent__state:empty{display:none}
.pv-consent__act{display:flex;gap:8px}
.pv-consent__btn{
  flex:1 1 0;min-height:44px;padding:0 14px;border-radius:var(--radius-btn);white-space:nowrap;
  font:inherit;font-size:var(--fs-m);font-weight:500;cursor:pointer;
  color:var(--white);background:transparent;border:1px solid var(--line-strong);
  transition-property:background-color,color;transition-duration:var(--t-press);transition-timing-function:var(--ease);
}
@media (hover:hover){ .pv-consent__btn:hover{background:color-mix(in srgb,var(--white) 8%,transparent)} }
.pv-consent__btn--primary{background:var(--btn-light);color:var(--ink);border-color:transparent}
@media (hover:hover){ .pv-consent__btn--primary:hover{background:var(--btn-light-hover)} }
.pv-consent__x{
  position:absolute;top:8px;right:8px;width:44px;height:44px;display:grid;place-items:center;
  border-radius:var(--radius-btn);background:none;border:0;cursor:pointer;
  color:var(--gray-3);font:inherit;font-size:22px;line-height:1;
  transition-property:color,background-color;transition-duration:var(--t-press);transition-timing-function:var(--ease);
}
@media (hover:hover){ .pv-consent__x:hover{color:var(--white);background:color-mix(in srgb,var(--white) 8%,transparent)} }
.pv-consent :is(a,button):focus-visible{outline:var(--focus-ring-dark);outline-offset:3px}
@media (prefers-reduced-motion:no-preference){
  .pv-consent{transition:opacity var(--t-fast) var(--ease-out),transform var(--t-mid) var(--ease-out)}
  .pv-consent:not(.is-open){opacity:0;transform:translateY(16px)}
  .pv-consent.is-leaving{transition-duration:.25s;transition-timing-function:var(--ease-in)}
}
/* the home page at every width (b106 S14): below 1024 px the bottom sheet covered the hero's only button */
html.pv-consent-top .pv-consent{top:calc(var(--bar-h) + var(--pv-gutter));bottom:auto}
@media (prefers-reduced-motion:no-preference){ html.pv-consent-top .pv-consent:not(.is-open){transform:translateY(-16px)} }

/* b112 — the consent gate. Until the visitor has answered, the panel is a modal and the page behind it is
   inert: there is no way past it that records nothing. Both answers stay one click and equally prominent,
   because a wall that only takes yes is not a choice. Centred with inset + margin, never with a transform,
   so the entrance above still has its translateY to leave from. No backdrop-filter: the perf gate budgets
   that at zero, and a flat scrim does the same job. */
html.pv-consent-gate{overflow:hidden}
html.pv-consent-gate body{padding-bottom:0}
/* The two z-indexes sit together on purpose. The panel does not keep the --pv-consent-z token at runtime:
   frames.css (b111) puts .pv-consent in the bar layer at 60, and a scrim derived from the token landed on
   top of it. Written as a pair, they cannot drift apart again. */
html.pv-consent-gate::before{content:"";position:fixed;inset:0;z-index:98;
  background:color-mix(in srgb,var(--bg-darker) 82%,transparent)}
html.pv-consent-gate .pv-consent{z-index:99;left:0;right:0;top:0;bottom:0;margin:auto;height:max-content;
  width:min(560px,calc(100% - 2 * var(--pv-gutter)))}
html.pv-consent-gate .pv-consent__x{display:none}
html.pv-consent-gate .pv-consent__copy{padding-right:0}
/* As a modal it is read, not glanced at: the heading comes back out of the screen-reader-only box, the
   sentence about what is stored is shown in full, and the two answers sit side by side at the foot.
   From 1024 px the bar layout turns all of that off, so these have to win there too. */
html.pv-consent-gate .pv-consent__in{flex-direction:column;align-items:stretch;gap:18px;padding:26px 26px 24px}
html.pv-consent-gate .pv-consent__title{position:static;width:auto;height:auto;overflow:visible;
  clip-path:none;white-space:normal;font-size:var(--fs-l);font-weight:600;letter-spacing:-.01em;
  text-transform:none;color:var(--white);line-height:1.15}
html.pv-consent-gate .pv-consent__txt{margin-top:10px;font-size:var(--fs-s)}
html.pv-consent-gate .pv-consent__more{display:inline}
html.pv-consent-gate .pv-consent__act{flex:none;display:grid;grid-template-columns:1fr 1fr;gap:10px}
html.pv-consent-gate .pv-consent__btn{min-height:48px}
/* a short landscape phone: the hero title fills the top-left and its button the bottom-left, so the card takes the
   right half of the top (measured at 844x390, b106 round 2) */
@media (max-width:1023px) and (max-height:500px) and (orientation:landscape){
  html.pv-consent-top .pv-consent{left:auto;right:max(var(--pv-gutter),env(safe-area-inset-right));width:min(var(--pv-consent-w),calc(50% - var(--pv-gutter)))}
}
/* b107 S08 (C06): from 1024 px the card is one row (text, two buttons, close; ≤ 80 px tall), so it no longer covers a
   page's first product image or the wizard's Next; the home page no longer takes the top placement (the ribbon field
   it sat over is gone, and the card covered the new hero's label there). */
@media (min-width:1024px){
  .pv-consent{width:min(940px,calc(100% - 2 * var(--pv-gutter)))}
  .pv-consent__in{flex-direction:row;align-items:center;gap:16px;padding:10px 60px 10px 20px}
  .pv-consent__copy{flex:1 1 auto;padding-right:0}
  .pv-consent__title{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
  .pv-consent__txt{margin-top:0}
  .pv-consent__more{display:none}
  .pv-consent__act{flex:none}
  .pv-consent__x{top:50%;transform:translateY(-50%)}
}
@media (max-width:1023px){
  .pv-consent{right:var(--pv-gutter);width:auto}
  .pv-consent__in{flex-direction:row;flex-wrap:wrap;align-items:center;gap:10px 12px;padding:12px 12px 12px 16px}
  .pv-consent__copy{flex:1 1 100%;padding-right:40px}
  .pv-consent__title{display:none}
  .pv-consent__txt{margin-top:0;font-size:var(--fs-xs)}
  .pv-consent__more{display:none}
  .pv-consent__act{flex:1 1 100%}
  .pv-consent__x{top:2px;right:2px}
}
/* keep focus clear of the open banner: scroll targets stop above it, and the page gets room to scroll its
   last controls out from underneath (the banner sits over the page's own bottom otherwise) */
html.pv-consent-open{scroll-padding-bottom:calc(var(--pv-banner-h,140px) + 2 * var(--pv-gutter))}
html.pv-consent-open body{padding-bottom:calc(var(--pv-banner-h,140px) + 2 * var(--pv-gutter))}
html.pv-consent-top.pv-consent-open{
  scroll-padding-top:calc(var(--bar-h) + var(--pv-banner-h,160px) + 2 * var(--pv-gutter));scroll-padding-bottom:auto}
html.pv-consent-top.pv-consent-open body{padding-bottom:0}
/* the footer's "Privacy settings" control reads like the Privacy link next to it */
.foot-consent{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}
.foot-consent:hover{color:var(--white,#f8f4ee)}
@media print{.pv-consent{display:none}}
/* b106 S14: no grey tap flash, no double-tap zoom delay on controls */
html{-webkit-tap-highlight-color:transparent}
:is(a,button,summary,label){touch-action:manipulation}
/* cookies.js moves focus to <main> after a first-visit choice; the landmark itself needs no ring */
main[tabindex="-1"]:focus{outline:none}

/* ============================================================
   FOOTER (step 11) — one grid on every page.
   Markup: site-source/partials/footer.html. These rules are scoped under footer.site-foot so
   they outrank the older per-page footer CSS each page still carries (same specificity would
   also win by order, this file loads after the page's <style>, but scoping makes it explicit).
   Columns: 4 at desktop / 2 at tablet / 1 at phone. The bar uses the same columns, so its
   items sit under the column heads instead of being spread by space-between.
============================================================ */
:root{
  --pv-foot-gap:clamp(24px,3vw,48px);
  --pv-foot-row:clamp(28px,4vw,44px);
}
footer.site-foot .foot-grid{
  display:grid;grid-template-columns:repeat(4,minmax(0,1fr));
  column-gap:var(--pv-foot-gap);row-gap:var(--pv-foot-row);
  padding-bottom:var(--space-m,clamp(44px,6vh,72px));
  align-items:start;
}
footer.site-foot .foot-col{display:flex;flex-direction:column;align-items:flex-start;gap:12px;min-width:0}
footer.site-foot .foot-cat{margin-bottom:4px;line-height:1.2}
footer.site-foot .foot-about p{margin-top:0;max-width:32ch}
footer.site-foot .foot-col a{line-height:1.3}
footer.site-foot .foot-now{margin-top:4px}
/* the watermark rests 6 % below its box (.watermark.in); the gap keeps it off the bar's hairline */
footer.site-foot .watermark{margin-bottom:clamp(20px,3vw,44px)}
footer.site-foot .foot-bar{
  display:grid;grid-template-columns:repeat(4,minmax(0,1fr));
  column-gap:var(--pv-foot-gap);row-gap:10px;align-items:baseline;
  font-size:var(--fs-s,.84375rem);
}
footer.site-foot .foot-bar > *{justify-self:start;min-width:0}
footer.site-foot .foot-bar .build{justify-self:end}
@media (max-width:1024px){
  footer.site-foot .foot-grid,
  footer.site-foot .foot-bar{grid-template-columns:repeat(2,minmax(0,1fr))}
  footer.site-foot .foot-bar .build{justify-self:end}
}
@media (max-width:600px){
  footer.site-foot .foot-grid{grid-template-columns:minmax(0,1fr)}
  /* r7: one line on a phone (it broke into an uneven 2 x 2) */
  footer.site-foot .foot-bar{display:flex;flex-wrap:wrap;align-items:center;gap:4px 24px}
}
@media (pointer:coarse){
  footer.site-foot .foot-consent{min-height:44px;display:inline-flex;align-items:center}
  footer.site-foot .foot-bar a{min-height:44px;display:inline-flex;align-items:center}
}

/* ============================================================
   TOP BAR — the quote link stays on phones (b105 step 18; b106 S02)
   Every page hid .tb-cta at 480 px and below, which left /about, /services and /privacy with no ask in the
   first screen of a phone. It is a text link with a rule under it (no pill); margin-left:auto from the page's
   own 900 px rule keeps it against the burger. At 360 px and below the bar's 28 px gaps drop to the phone
   gutter and the link reads "Get a quote" (the " fixed" span hides), so the burger is never clipped at 320 px
   (WCAG 1.4.10; b105 blocker 4: #burger spanned 292–336 px in a 320 px viewport).
============================================================ */
@media (max-width:480px){
  .topbar .tb-cta{display:inline-flex;align-items:center;white-space:nowrap;margin-right:6px}
}
@media (max-width:360px){
  .topbar .wrap{gap:var(--gutter)}
  .topbar .tb-cta{margin-right:0}
  /* b122: .tb-cta-x is gone. It hid the word "fixed" in "Get a fixed quote" below 360 px only, so every
     wider screen still read "fixed" after b120 took fixed pricing off the site. */
}

/* ============================================================
   b106 S02 — global rules
============================================================ */
/* b107: the b106 D1 grayscale rule (main img, main video, .grade) is gone; photos are colour, graded in the
   file (overhaul/qa/grade.mjs). */

/* headings never leave a one-word last line */
h1, h2{text-wrap:balance}

/* D3: a service hero's subline is the page's value proposition — body prose, not a micro label */
.ec-hero .ec-sub, .bi-hero .bi-sub{
  font-size:var(--fs-body);font-weight:400;letter-spacing:normal;text-transform:none;
  line-height:1.6;max-width:62ch;text-wrap:pretty;color:var(--gray-3);
}
[data-stage="light"] .ec-hero .ec-sub{color:var(--muted-on-light)}
/* D3: service heroes are left-aligned (web-design, web-development and ecommerce were centred; home, ai-assistants and
   brand-identity already read left). Equal specificity to the pages' own centring rules, and this file loads later. */
.ec-hero .ec-hero-in{text-align:left;justify-self:stretch;width:100%}   /* the hero grid centres a shrink-wrapped block: stretch it to the page edge */
.ec-hero .ec-sub{margin-inline:0}
.ec-hero .ec-hero-act{justify-content:flex-start}

/* statements: a short display lead + body prose, the service's own proof beside it (b105 ran 7–12 lines of
   52 px type at line-height 1.12 in the left half, with the right half empty) */
.pv-state{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr);gap:clamp(32px,5vw,96px);align-items:center}
:is(.ec-state,.bi-state) .pv-state p{grid-column:auto;max-width:none;margin:0}
:is(.ec-state,.bi-state) .pv-state .pv-state-lead{
  font-size:clamp(26px,2.9vw,42px);font-weight:500;line-height:1.2;letter-spacing:-.012em;max-width:24ch;text-wrap:balance}
:is(.ec-state,.bi-state) .pv-state .pv-state-body{
  margin-top:clamp(16px,1.8vw,26px);font-size:var(--fs-body);line-height:1.65;color:var(--gray-3);max-width:56ch;text-wrap:pretty}
[data-stage="light"] .pv-state .pv-state-body{color:var(--muted-on-light)}
/* a positioning line quoted verbatim can be long: smaller and wider, still ≤ 4 lines at 1440 */
:is(.ec-state,.bi-state) .pv-state--long .pv-state-lead{font-size:clamp(24px,2.3vw,34px);max-width:36ch}
.pv-state-proof{margin:0;justify-self:end;width:100%;border-radius:var(--radius-card);overflow:hidden;
  border:1px solid var(--line-strong);background:var(--panel)}
[data-stage="light"] .pv-state-proof{border-color:var(--line-dark)}
.pv-state-proof img{display:block;width:100%;height:auto}
.pv-state-proof--phone{max-width:300px;border-radius:30px}
.pv-state-proof--screen{max-width:620px}
@media (max-width:900px){
  .pv-state{grid-template-columns:minmax(0,1fr)}
  .pv-state-proof{justify-self:start}
  .pv-state-proof--phone{max-width:240px}
}
/* the mark rows lost their lone "+" glyph: the copy starts at the grid's first column */
.ec-markrow .ec-copy{grid-column:1 / span 8}
@media (max-width:900px){ .ec-markrow .ec-copy{grid-column:1 / -1} }

/* ============================================================================================
   Quote step (b116 S02) — the first screen of the quote wizard. Moved here verbatim from
   /contact's inline CSS so /contact and the landing's closing block share ONE component.
   Order kept: base rules, then the <= 480 px narrowing that overrides them.
   ============================================================================================ */
.q-progress{display:flex;gap:6px;width:180px;margin-left:auto}
.q-progress-seg{height:3px;border-radius:var(--radius-pill);background:var(--line-dark-strong);flex:1;transition:background .3s var(--ease)}
.q-progress-seg.done{background:var(--ink)}
.q-question{display:block;font-size:clamp(22px,2.6vw,30px);font-weight:600;letter-spacing:-.012em;color:var(--ink);margin-bottom:22px}
.svc-pick-hint{font-size:var(--fs-s);color:var(--muted-on-light);margin-bottom:18px}
.svc-pick{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
@media (max-width:640px){ .svc-pick{grid-template-columns:minmax(0,1fr)} }   /* b106: no "AI / Assistants" wraps at 390. b107 S08 (C46): 640, not 420 — with the 01-05 index shown at every width, two columns wrapped a name at every width up to 580 ("Web / Development" measured at 481-580) */
.svc-card{position:relative;display:block;cursor:pointer;min-width:0}
.svc-card:last-child:nth-child(odd){grid-column:1 / -1}
.svc-card input{position:absolute;opacity:0;width:1px;height:1px;margin:0}
.svc-card-in{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:12px;
  min-height:64px;padding:12px 14px 12px 16px;border:1px solid var(--field-border);border-radius:var(--radius-img);
  transition:background var(--t-micro) var(--ease),border-color var(--t-micro) var(--ease)}
.svc-card-n{font-size:var(--fs-micro);letter-spacing:var(--ls-caps);color:var(--muted-on-light);font-variant-numeric:tabular-nums}
.svc-card-k{font-size:var(--fs-b);font-weight:600;color:var(--ink);line-height:1.25}
.svc-card-tick{position:relative;width:22px;height:22px;border-radius:50%;border:1px solid var(--field-border)}
.svc-card-tick::after{content:"";position:absolute;left:7px;top:3px;width:5px;height:10px;border:solid var(--ink);border-width:0 2px 2px 0;
  transform:rotate(45deg) scale(0);transition:transform var(--t-micro) var(--ease-settle)}
.svc-card input:checked + .svc-card-in{background:var(--ink);border-color:var(--ink)}
.svc-card input:checked + .svc-card-in .svc-card-k{color:var(--white)}
.svc-card input:checked + .svc-card-in .svc-card-n{color:var(--gray-3)}
.svc-card input:checked + .svc-card-in .svc-card-tick{background:var(--white);border-color:var(--white)}
.svc-card input:checked + .svc-card-in .svc-card-tick::after{transform:rotate(45deg) scale(1)}
.svc-card input:not(:checked) + .svc-card-in:hover{border-color:var(--ink)}   /* b116: stronger than rest, from tokens */
.svc-card input:focus-visible + .svc-card-in{outline:2px solid var(--focus-light);outline-offset:3px}
.svc-pick-fs .q-err.show{margin-top:10px}
.q-err{
  display:flex;align-items:center;gap:8px;font-size:var(--fs-xs);color:var(--ink);
  max-height:0;opacity:0;overflow:hidden;margin-top:0;
  transition:opacity var(--t-micro) var(--ease),max-height var(--t-micro) var(--ease),margin-top var(--t-micro) var(--ease);
}
.q-err svg{width:8px;height:13px;flex:none}
.q-err circle{fill:currentColor}
/* the invalid state is per FIELD, not per step: a step can hold two inputs now, and
   step-scoped selectors reddened both borders and opened both messages for one bad value */
.q-err.show{max-height:64px;opacity:1;margin-top:8px}   /* b106: the fix-it messages run to 2–3 lines on a phone */
@media (max-width:480px){
  .svc-card-in{min-height:58px;padding:10px 10px 10px 12px;gap:8px}   /* b107 S08 (C46): the 01-05 index shows at every width, as on /services */
  .svc-card-k{font-size:var(--fs-m)}
}

/* b126 r5 (overnight audit): the section rhythm, one step tighter. Two --space-xl paddings met as ~300 px of
   empty black between sections at 1440 x 900, a third of the screen. */
:root{--space-l:clamp(64px,8vh,104px);--space-xl:clamp(72px,8.5vh,124px)}
/* the build stamp stays in the markup (tools/stamp.mjs, do-not-touch: .build) but is not marketing copy */
.build{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}

/* b126 r5 — /about "The work": four frames, two per row from 768, a row you swipe below that */
.ab-work{padding-top:var(--space-l)}
.ab-work .label{display:block}
.ab-work-list{list-style:none;margin:clamp(28px,3vw,44px) 0 0;padding:0;display:grid;gap:clamp(28px,3vw,44px) clamp(16px,2vw,28px);grid-template-columns:repeat(2,minmax(0,1fr))}
.ab-work-link{display:block;color:var(--white);text-decoration:none}
.ab-work-img{display:block;overflow:hidden;border-radius:var(--radius-img);border:1px solid var(--line);background:var(--panel)}
.ab-work-img img{display:block;width:100%;height:auto;aspect-ratio:1600 / 1000;object-fit:cover;object-position:50% 0;transition:transform var(--t-mid) var(--ease-out)}
.ab-work-link:hover .ab-work-img img,.ab-work-link:focus-visible .ab-work-img img{transform:scale(1.025)}
.ab-work-name{display:inline-block;margin-top:14px;font-size:var(--fs-l);font-weight:600}
.ab-work-meta{margin-left:10px;color:var(--muted-on-dark)}
@media (max-width:767px){
  .ab-work-list{grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:82%;overflow-x:auto;scroll-snap-type:x mandatory;
    margin-inline:calc(-1 * var(--pad-x));padding-inline:var(--pad-x);scroll-padding-inline:var(--pad-x);scrollbar-width:none}
  .ab-work-list::-webkit-scrollbar{display:none}
  .ab-work-list li{scroll-snap-align:start}
  .ab-work-meta{display:block;margin:4px 0 0}
}
@media (prefers-reduced-motion:reduce){.ab-work-img img{transition:none}}

/* b126 r5 (jury 2) — footer on phones: the two link lists side by side, and the columns close up
   (it ran two and a half screens as one column) */
@media (max-width:600px){
  footer.site-foot .foot-grid{grid-template-columns:repeat(2,minmax(0,1fr));row-gap:32px}
  footer.site-foot .foot-grid > .foot-about,
  footer.site-foot .foot-grid > .foot-col:last-child{grid-column:1 / -1}
  footer.site-foot .foot-col{gap:6px}
}

/* b126 r5 — a case page's three decisions, between the live tour and the screens */
.cs-notes{padding:var(--space-m) 0 0}
.cs-notes-list{list-style:none;margin:clamp(18px,2vw,28px) 0 0;padding:0;display:grid;gap:clamp(24px,3vw,48px);
  grid-template-columns:repeat(3,minmax(0,1fr));counter-reset:csn}
.cs-notes-list li{border-top:1px solid var(--line-strong);padding-top:clamp(14px,1.4vw,20px);counter-increment:csn}
.cs-notes-list li::before{content:"0" counter(csn);display:block;font-family:var(--font-mono);font-size:var(--fs-micro);
  letter-spacing:.08em;color:var(--muted-on-dark);margin-bottom:10px}
.cs-notes-list h3{margin:0;font-size:clamp(19px,1.6vw,24px);font-weight:600;letter-spacing:-.012em;line-height:1.2;color:var(--white)}
.cs-notes-list p{margin:10px 0 0;font-size:var(--fs-body);line-height:1.6;color:var(--gray-3);max-width:40ch}
@media (max-width:900px){ .cs-notes-list{grid-template-columns:none} }

/* b126 r6 — a lazy picture fades in when it has arrived (assets/js/motion.js), never pops */
img.pv-fade{opacity:0;transition:opacity .7s var(--ease-out,cubic-bezier(.16,1,.3,1))}
img.pv-fade.pv-fade-in{opacity:1}
@media (prefers-reduced-motion:reduce){ img.pv-fade{opacity:1;transition:none} }

/* ============================================================
   r7 — THE CLOSING CARD, one component on every page (services, about, work)
   Heading "Ready when you are. / Send the brief.", the pill, the email line.
   Pages keep their own copies of these rules; these win so no page drifts.
============================================================ */
body .ab-cta{
  margin:var(--space-l) 0 var(--space-xl);
  border:1px solid var(--line);border-radius:var(--radius-shell);
  background:var(--panel);
  padding:clamp(26px,3.2vw,56px);
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,2fr);
  gap:clamp(26px,4vw,70px);align-items:center;
}
body .ab-cta-head h2{font-size:clamp(28px,3vw,44px);line-height:1.1;font-weight:600;letter-spacing:-.022em;margin:0;max-width:none}
body .ab-cta-head .dim{color:var(--dim-on-dark)}
body .ab-cta-act{margin-top:0;display:flex;flex-wrap:wrap;align-items:center;gap:16px 24px}
body .ab-cta-act .btn{margin-top:0}
body .ab-cta-note{margin:0;font-size:var(--fs-m);color:var(--muted-on-dark)}
body .ab-cta-note a{color:var(--white);text-decoration:underline;text-decoration-color:var(--line-strong);text-underline-offset:3px}
body .ab-cta-note a:hover,body .ab-cta-note a:focus-visible{text-decoration-color:currentColor}
@media (max-width:940px){ body .ab-cta{grid-template-columns:1fr;gap:24px} }
@media (max-width:640px){ body .ab-cta-act .btn{width:100%;justify-content:center} }
/* work pages: the card replaces the old plain block; the "more work" row stays under it */
.cs-cta{border-top:0}
.cs-cta .ab-cta{margin-top:0;margin-bottom:0}

/* r7 — the top bar's "Get a quote" is the same pill as every button in the page, drawn in the bar's own
   colour (currentColor) so it reads on the dark and the light stages alike. Was a text link with a rule. */
.topbar a.tb-cta{
  display:inline-flex;align-items:center;white-space:nowrap;
  padding:9px 18px;border:1px solid currentColor;border-radius:var(--radius-pill);
  border-bottom-width:1px;line-height:1.1;font-weight:500;
}
.topbar a.tb-cta:hover{opacity:1;background:currentColor}
.topbar.on-dark a.tb-cta:hover .tb-cta-t{color:var(--bg-dark)}
.topbar:not(.on-dark) a.tb-cta:hover .tb-cta-t{color:var(--bg-light)}
@media (pointer:coarse){ .topbar a.tb-cta{min-height:40px} }
@media (max-width:360px){ .topbar a.tb-cta{padding:8px 14px} }

/* r7 — case-study tour player, shared by all six case pages (copied from the three that had it) */
/* b121 — the frame was 16/9 while the tour is 16/10, which is what cut the site's headline in half;
   the radius broke the square-corner rule at the same time. The frame now matches the capture, the
   video is never cropped (contain, asserted in verify-tours), and the corners are square. */
.cs-frame--tour video{display:block;width:100%;height:auto;aspect-ratio:16 / 10;object-fit:cover;
  border-radius:0;background:var(--ink,var(--bg-darker));border:1px solid var(--line)}
@media (max-width:767px){ .cs-frame--tour video{aspect-ratio:780 / 974} }
.cs-frame--tour video{border:0;border-radius:0}
/* b121 S03 — the disc and the progress hairline. The disc is the hero's disc: 32 px, icon only, 0.6
   resting and 1 on hover or focus. The hairline is one pixel along the bottom edge of the frame, scaled
   by transform so it costs nothing per frame. Both live INSIDE the frame: the rule for this card is that
   nothing sits under it. */
.sbp-frame{position:relative}
.sbp-disc{position:absolute;right:16px;bottom:16px;width:32px;height:32px;display:grid;place-items:center;
  padding:0;border:1px solid color-mix(in srgb,var(--white) 45%,transparent);border-radius:999px;
  background:color-mix(in srgb,var(--bg-dark) 62%,transparent);color:var(--white);cursor:pointer;
  opacity:.6;transition:opacity .35s var(--ease-out),border-color .35s var(--ease-out)}
.sbp-frame:hover .sbp-disc,.sbp-disc:focus-visible{opacity:1;border-color:var(--white)}
.sbp-disc svg{width:12px;height:12px;fill:currentColor}
.sbp-disc .i-pause{display:none}
.sbp-disc[aria-pressed="true"] .i-play{display:none}
.sbp-disc[aria-pressed="true"] .i-pause{display:block}
.sbp-bar{position:absolute;left:0;right:0;bottom:0;height:1px;background:var(--white);
  transform:scaleX(0);transform-origin:0 50%;pointer-events:none}
@media (prefers-reduced-motion:reduce){ .sbp-disc{transition:none} }
/* footage recorded at 16:9 keeps its own shape, on every width (no portrait capture exists) */
.cs-frame--wide.cs-frame--tour video{aspect-ratio:16 / 9}
@media (max-width:767px){ .cs-frame--wide.cs-frame--tour video{aspect-ratio:16 / 9} }

/* r8 — one FAQ layout on every page that has one (home, AI assistants, web development): the heading on
   the left, held while the questions scroll past on the right, all on the page grid */
@media (min-width:1024px){
  main section#faq > .wrap{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);column-gap:clamp(40px,5vw,96px);align-items:start}
  main section#faq > .wrap > *{grid-column:2;min-width:0}
  main section#faq > .wrap > .sec-head{grid-column:1;grid-row:1 / span 4;position:sticky;top:calc(var(--bar-h) + 32px)}
  main section#faq > .wrap > .faq-list{margin-top:0;grid-template-columns:minmax(0,1fr);max-width:none}
}

/* r9 — case pages: "more work" comes before the closing card (it sat in 10px under it, after the ask) */
.cs-cta .cs-next{margin:0 0 var(--space-m);padding:22px 0 0;border-top:1px solid var(--line)}
.cs-cta .cs-next .cs-back{font-size:var(--fs-s)}
.cs-cta .ab-cta{margin-top:0}

/* ============================================================================================
   b126 r10 — ONE MOTION LANGUAGE (every page now carries the gate and the runtime; see partials/motion-includes)

   The site's one gesture is "something arrives from below while what it replaces gives way": the case-page
   flood, the premium invite, the section reveals. Three places did not speak it yet:

   1. Moving between pages was a hard cut everywhere except into a case study. Every page now opts into
      cross-document view transitions: the old page lifts away and dims in 260 ms, the new one rises 12 px
      into place on the site's --ease-out. The top bar is its own group, so it holds still while the page
      under it changes, and its links do not re-enter on an internal navigation (html.pv-internal, set by
      the gate from a same-origin referrer). The case flood (frames.css) outranks this by specificity.
   2. The inner pages' headings were painted flat (or faded as one block by the section observer). The
      page head now enters on the same beat on every inner page: label, then the heading uncovered from
      below, then the lede, then the actions, 90 ms apart. Pure CSS from the first paint, not keyed on the
      font gate: waiting for the gate cost ~1 s of LCP on a slow phone (Lighthouse /about 98 -> 93).
   3. Reduced motion gets none of it: no gate class, no transition, no entrance.
   ============================================================================================ */
@view-transition { navigation: auto; }
.topbar { view-transition-name: pv-topbar; }
@keyframes pv-vt-out { to { opacity: 0; transform: translateY(-10px) scale(.996); } }
@keyframes pv-vt-in { from { opacity: 0; } }   /* the page does not travel; its head does (below) */
/* b126 r15 (night jury): the two pages now overlap instead of passing through black — the old one leaves in 150 ms,
   the new one arrives from 120 ms (a 30 ms overlap, never a double exposure), 300 ms long (was 200 out, a black hold, then 520 in) */
html.pv-anim::view-transition-old(root) { animation: 150ms cubic-bezier(.4,0,1,1) both pv-vt-out; }
html.pv-anim::view-transition-new(root) { animation: 300ms var(--ease-out, cubic-bezier(.16,1,.3,1)) 120ms both pv-vt-in; }
html.pv-anim::view-transition-group(pv-topbar) { animation-duration: 300ms; }
html.pv-internal .topbar.ready :is(.tb-links a, .tb-cta) { animation: none !important; }
html.pv-internal.js:not([data-ready]) .topbar.ready :is(.tb-links a, .tb-cta) { opacity: 1 !important; }

/* arriving through a page transition, the head waits for the new page to be up before it moves (--pv-head-d).
   The stagger is a custom property per child (--pv-i), so the one shorthand below carries every delay: a
   separate animation-delay rule loses to the shorthand's own reset. */
:root { --pv-head-d: 0ms; }
html.pv-internal { --pv-head-d: 0ms; }   /* b126 r15: 240 -> 120; r22: 0 - the new page fades in from 120 ms anyway, and with a wait on top the screen sat black for ~200 ms between pages (night jury) */
/* from .01, not 0: Chrome never counts a text block first painted at opacity 0 for LCP, so a 0-start entrance hands
   LCP to whatever small thing paints last (measured: the top bar's button at 3-4 s). At .01 the block is invisible
   and still counted at first paint. */
@keyframes pv-head-up { from { opacity: .01; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes pv-head-lcp { from { transform: translateY(14px); clip-path: inset(0 0 100% 0); } to { transform: none; clip-path: inset(-12% -6% -30% -6%); } }
/* the wipe runs top to bottom (the bottom edge of the clip travels down), so a two- or three-line heading is read
   in order while the whole block rises into place: line one first, never the last line alone */
@keyframes pv-head-h1 {
  from { opacity: .01; transform: translateY(.3em); clip-path: inset(-12% -6% 100% -6%); }
  to   { opacity: 1; transform: none; clip-path: inset(-12% -6% -30% -6%); }
}
:is(.svc-page-head, .ec-hero-in, .ai-hero-head, .ab-hero > .wrap, .quote-intro-block, .priv-head, .cs-head > .wrap, .bi-hero-copy) > :nth-child(2) { --pv-i: 90ms; }
:is(.svc-page-head, .ec-hero-in, .ai-hero-head, .ab-hero > .wrap, .quote-intro-block, .priv-head, .cs-head > .wrap, .bi-hero-copy) > :nth-child(3) { --pv-i: 180ms; }
:is(.svc-page-head, .ec-hero-in, .ai-hero-head, .ab-hero > .wrap, .quote-intro-block, .priv-head, .cs-head > .wrap, .bi-hero-copy) > :nth-child(4) { --pv-i: 270ms; }
:is(.svc-page-head, .ec-hero-in, .ai-hero-head, .ab-hero > .wrap, .quote-intro-block, .priv-head, .cs-head > .wrap, .bi-hero-copy) > :nth-child(5) { --pv-i: 360ms; }
:is(.svc-page-head, .ec-hero-in, .ai-hero-head, .ab-hero > .wrap, .quote-intro-block, .priv-head, .cs-head > .wrap, .bi-hero-copy) > :nth-child(n+6) { --pv-i: 450ms; }
html.pv-anim :is(.svc-page-head, .ec-hero-in, .ai-hero-head, .ab-hero > .wrap, .quote-intro-block, .priv-head, .cs-head > .wrap, .bi-hero-copy) > :not(h1.in) {
  animation: pv-head-up .9s var(--ease-out, cubic-bezier(.16,1,.3,1)) calc(var(--pv-head-d) + var(--pv-i, 0ms)) both;
}
html.pv-anim :is(.svc-page-head, .ec-hero-in, .ai-hero-head, .ab-hero > .wrap, .quote-intro-block, .priv-head, .cs-head > .wrap, .bi-hero-copy) > h1:not(.in) {
  animation: pv-head-h1 1.1s var(--ease-out, cubic-bezier(.16,1,.3,1)) calc(var(--pv-head-d) + var(--pv-i, 0ms)) both;
}
@media (prefers-reduced-motion: reduce) {
  html:not([data-pv-motion="1"])::view-transition-old(root),
  html:not([data-pv-motion="1"])::view-transition-new(root) { animation: none; }
}
/* Lenis's own stylesheet, the two lines this site needs: a stopped Lenis (the consent gate) stops the page too */
html.lenis.lenis-stopped { overflow: clip; }
html.lenis.lenis-smooth iframe { pointer-events: none; }
/* Under Lenis the store strip's mandatory snap pulled every trackpad swipe back to its first card (Chrome
   re-snaps it on each smooth page frame). Free horizontal scroll there; the arrows still step one card. */
html.lenis .ec-strip { scroll-snap-type: none; }
/* section headings (motion.js marks the ones below the first screen) — the page head's gesture, on arrival */
html.pv-anim h2.pv-h:not(.pv-h-in) { opacity: 0; }
html.pv-anim h2.pv-h.pv-h-in { animation: pv-head-h1 1.1s var(--ease-out, cubic-bezier(.16,1,.3,1)) both; }
/* /about: the pinned process scene is the head's last beat, not something painted before the heading */
html.pv-anim #tlStage { animation: pv-head-up .9s var(--ease-out, cubic-bezier(.16,1,.3,1)) calc(var(--pv-head-d) + 360ms) both; }
/* phone menu: the backdrop is opaque (150 ms) before the first link starts to rise, so the links never sit on
   top of the page's own headline */
body .menu-ov.open { transition: opacity .15s var(--ease); }
body .menu-ov.open a { transition-delay: .14s; }
body .menu-ov.open a:nth-child(2) { transition-delay: calc(.14s + var(--stagger-fine) * 1); }
body .menu-ov.open a:nth-child(3) { transition-delay: calc(.14s + var(--stagger-fine) * 2); }
body .menu-ov.open a:nth-child(4) { transition-delay: calc(.14s + var(--stagger-fine) * 3); }
body .menu-ov.open a:nth-child(5) { transition-delay: calc(.14s + var(--stagger-fine) * 4); }
body .menu-ov.open a:nth-child(6) { transition-delay: calc(.14s + var(--stagger-fine) * 5); }

/* the top bar's own entrance (NAV 1, per page) holds its links at opacity 0 until the font gate; .01 keeps them out of
   sight and out of the LCP race's last place */
.js:not([data-ready]) .topbar.ready :is(.tb-links a, .tb-cta) { opacity: .01; }

/* ============================================================================================
   b126 r11 — the second pass on the jury's motion notes
   ============================================================================================ */
/* phone menu: it opens out of the button that opened it (a circle grown from the top-right corner) and closes
   back into it, links first, so the page under it is never seen through a half-faded sheet */
html.pv-anim body .menu-ov { opacity: 1; clip-path: circle(0 at calc(100% - 38px) 32px);
  transition: clip-path .5s var(--ease-in-out), visibility 0s .5s; }
html.pv-anim body .menu-ov.open { clip-path: circle(150vmax at calc(100% - 38px) 32px);
  transition: clip-path .62s var(--ease-in-out), visibility 0s; }
html.pv-anim body .menu-ov:not(.open) a { transition-duration: .18s; transition-delay: 0s; }
html.pv-anim body .menu-ov.open a { transition-delay: .2s; }
html.pv-anim body .menu-ov.open a:nth-child(2) { transition-delay: calc(.2s + var(--stagger-fine) * 1); }
html.pv-anim body .menu-ov.open a:nth-child(3) { transition-delay: calc(.2s + var(--stagger-fine) * 2); }
html.pv-anim body .menu-ov.open a:nth-child(4) { transition-delay: calc(.2s + var(--stagger-fine) * 3); }
html.pv-anim body .menu-ov.open a:nth-child(5) { transition-delay: calc(.2s + var(--stagger-fine) * 4); }
html.pv-anim body .menu-ov.open a:nth-child(6) { transition-delay: calc(.2s + var(--stagger-fine) * 5); }
/* a press is felt: every button and the top bar's call to action give a little under the finger, in 150 ms */
html body :is(.btn, .tb-cta, .q-next, .q-send, .offer-pick, .q-edit):active {
  transform: translateY(0) scale(.975); transition-duration: var(--t-press); }
/* the closing card's section: the card already carries its own top margin (--space-l), so the section adds none,
   and the card no longer stacks a second --space-xl under itself on top of the section's own (263px of black
   above the card and 200px below it, measured on /services/ecommerce and /services/brand-identity at 1440) */
body :is(.ec-cta, .bi-cta) { padding-top: 0; }
body :is(.ec-cta, .bi-cta) .ab-cta { margin-bottom: 0; }
/* case pages: the tour's caption sits inside the framed card, so it takes the card's inner padding instead of
   running 3 px from its edge (jury: foxcott and apex at 1440) */
body .cs-frame figcaption { margin: 0; padding: 14px clamp(16px, 1.8vw, 24px) 16px; max-width: none; }
/* case pages, wide screens: the head is set as a spread — the headline on the left seven columns, the client line
   and the standfirst on the right five, bottoms aligned — instead of a 685 px column with 580 px of nothing beside
   it on the pages meant to show the craft (jury: foxcott, apex, youngpl). Phones and tablets keep the column. */
@media (min-width:1100px){
  body .cs-head > .wrap{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);column-gap:clamp(40px,5vw,96px);align-items:end}
  body .cs-head > .wrap > *{grid-column:1 / -1}
  body .cs-head > .wrap > .cs-title{grid-column:1;grid-row:2 / span 2;max-width:15ch}
  body .cs-head > .wrap > .cs-meta{grid-column:2;grid-row:2;margin-top:clamp(18px,2.4vw,32px)}
  body .cs-head > .wrap > .cs-sum{grid-column:2;grid-row:3;max-width:34ch}
  body .cs-head > .wrap > .cs-frame{grid-row:4}
}
/* case pages: the next project is a card with its own picture, not a grey underlined tag string (jury) */
body .cs-nextcard{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(20px,3vw,48px);align-items:end;
  padding:clamp(20px,2.4vw,32px) 0;border-top:1px solid var(--line);color:var(--white);text-decoration:none}
body .cs-nextcard-media{display:block;overflow:hidden;border-radius:var(--radius-img);aspect-ratio:16 / 10;background:var(--panel);border:1px solid var(--line)}
body .cs-nextcard-media img{display:block;width:100%;height:100%;object-fit:cover;object-position:50% 0;transition:transform .9s var(--ease-out)}
body .cs-nextcard-text{display:flex;flex-direction:column;align-items:flex-start;gap:10px;padding-bottom:4px}
body .cs-nextcard-t{font-size:clamp(30px,3.4vw,48px);line-height:1.02;letter-spacing:-.025em;font-weight:600}
body .cs-nextcard-m{font-size:var(--fs-m);color:var(--muted-on-dark)}
body .cs-nextcard:focus-visible{outline:var(--focus-ring-dark);outline-offset:6px;border-radius:var(--radius-img)}
@media (hover:hover) and (pointer:fine){
  body .cs-nextcard:hover .cs-nextcard-media img{transform:scale(1.035)}
  body .cs-nextcard:hover .cs-nextcard-t{text-decoration:underline;text-underline-offset:6px;text-decoration-thickness:2px}
}
@media (max-width:640px){ body .cs-nextcard{grid-template-columns:1fr} }
body .cs-cta .cs-nextcard + .cs-next{margin-top:0}

/* ============================================================================================
   b126 r12 — ONE TOP BAR (owner: "unify the navbar"). Every page carries its own copy of the bar's rules;
   these come last and win, so the bar is the same bar on every route.
   ============================================================================================ */
/* the links sit on a 32 px rhythm and the pill 40 px after them — it hung 100 px off the cluster, off the grid */
@media (min-width:1025px){
  html body .topbar .tb-links{gap:32px}
  html body .topbar .tb-cta{margin-left:12px}   /* + the wrap's own 28 px gap = 40 px */
}
/* the current page and the hover share one mark: a solid 1 px rule that grows from the left (the dotted row read
   like an abbreviation marker), 300 ms on the site's in-out curve */
html body .tb-links a::after{
  height:1px;bottom:12px;background:currentColor;
  transform:scaleX(0);transform-origin:right;transition:transform .3s cubic-bezier(.65,0,.35,1);
}
html body .tb-links a:is(:hover,:focus-visible,[aria-current="page"],[aria-current="true"])::after{transform:scaleX(1);transform-origin:left}
html body .tb-links a:is([aria-current="page"],[aria-current="true"]){opacity:1}
/* one bar height per width: 64 px on a phone everywhere (it was 64 on two pages and 76 on the rest) */
@media (max-width:640px){
  :root{--bar-h:64px}
  html body .menu-ov a{font-size:26px}
}
/* the footer wordmark sits on the page grid — the same left and right edges as the content and the bar line under
   it — instead of running 4-14 px from the window's edges, where it read as clipped */
html body footer.site-foot .watermark svg{width:calc(min(1420px, 100%) - 2 * var(--pad-x));margin-inline:auto}
/* carousel arrows: a plain drawn chevron instead of the five-dot pixel arrow, which in a small circle (and dimmed at
   the strip's end) read as a loading spinner (jury). The pixel arrow stays on the buttons, where it reads as an arrow. */
html body :is(.bi-arrow, .ec-arrow) .px-arrow{display:none}
html body :is(.bi-arrow, .ec-arrow)::before{
  content:"";width:9px;height:9px;border-top:1.5px solid currentColor;border-right:1.5px solid currentColor;
  transform:translateX(-2px) rotate(45deg);transition:transform var(--t-fast) var(--ease-out)}
html body :is(.bi-arrow--prev, .ec-arrow--prev)::before{transform:translateX(2px) rotate(-135deg)}
@media (hover:hover) and (pointer:fine){
  html body :is(.bi-arrow--next, .ec-arrow--next):hover::before{transform:translateX(0) rotate(45deg)}
  html body :is(.bi-arrow--prev, .ec-arrow--prev):hover::before{transform:translateX(0) rotate(-135deg)}
}
/* the closing card: the ask sits against the card's right edge, so the card reads as heading ... action instead of
   leaving its right 40 % empty (jury). One column below 940 px keeps everything on the left. */
@media (min-width:941px){
  body .ab-cta{grid-template-columns:minmax(0,1fr) auto}
  body .ab-cta-body{justify-self:end}
  body .ab-cta-act{justify-content:flex-end}
}
/* home, phones: the ribbons sat in the top third and left ~220 px of black between them and the headline (jury).
   The scene grows from its top edge (no hard crop line appears, the ribbons already run off the top), so the
   sculpture comes down to meet the words. */
@media (max-width:640px){
  html body .hero > .hero-scene{transform:scale(1.38);transform-origin:50% 0}
}

/* ============================================================================================
   b126 r14 — the quote wizard (night jury)
   ============================================================================================ */
/* the picked card's words changed colour in one frame while its fill was still fading in, so for ~100 ms the label
   sat cream on cream; words, number and tick now fade on the same 150 ms as the fill */
body .svc-card-k, body .svc-card-n{transition:color var(--t-press) var(--ease)}
body .svc-card-tick{transition:background-color var(--t-press) var(--ease),border-color var(--t-press) var(--ease)}
/* the steps' box clips (its height is animated), and it clipped the inputs' focus ring and the Next pill's shadow at
   the sides; 10 px of room on each side, given back with a negative margin so nothing moves */
body .q-steps-wrap{padding-inline:10px;margin-inline:-10px}
/* the size question: three options with a line each, none chosen for the visitor */
body .tier-select{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
@media (max-width:640px){ body .tier-select{grid-template-columns:minmax(0,1fr)} }
body .tier-select .tp{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:4px;
  min-height:76px;padding:14px 18px;border-radius:var(--radius-img);font-weight:600;line-height:1.25}
body .tier-select .tp-d{font-size:var(--fs-s);font-weight:400;color:var(--muted-on-light);transition:color var(--t-press) var(--ease)}
body .tier-select input:checked + .tp .tp-d{color:var(--gray-3)}
body .tier-fieldset .q-err.show{margin-top:12px}
/* the review's free-text box: the one place to say what the site has to do */
body .q-notes{margin-top:28px}
body .q-notes-l{display:block;margin-bottom:10px;font-size:var(--fs-b);font-weight:600;color:var(--ink)}
body .q-notes-l span{margin-left:6px;font-size:var(--fs-s);font-weight:400;color:var(--muted-on-light)}
body .q-notes textarea{display:block;width:100%;min-height:104px;resize:vertical;padding:12px 14px;
  border:1px solid var(--field-border);border-radius:var(--radius-img);background:transparent;
  font:inherit;font-size:var(--fs-b);line-height:1.5;color:var(--ink)}
body .q-notes textarea::placeholder{color:var(--placeholder)}
body .q-notes textarea:focus-visible{outline:2px solid var(--focus-light);outline-offset:3px}
/* on /contact the bar's pill carried the old dotted current-page row across its bottom border (read as a glitch);
   the "Contact" link already marks the page, so the pill stays the plain pill it is everywhere else */
html body .topbar a.tb-cta[aria-current="page"]{background:none;border-color:currentColor}
/* b126 r23: that rule outranked the pill's hover fill, so on /contact the hover turned the words dark on an empty pill
   (owner's screenshot); the hover fill is restored at the same weight */
html body .topbar a.tb-cta[aria-current="page"]:hover{background:currentColor}
/* Skip was a 26 px wide target */
body .q-skip{min-width:44px;padding-inline:12px;justify-content:center}

/* ============================================================================================
   b126 r15 — motion, second night pass
   ============================================================================================ */
/* the bar's links and pill enter on the first page of a visit only (html.pv-nav-seen, set by the gate) */
html.pv-nav-seen .topbar.ready :is(.tb-links a, .tb-cta) { animation: none !important; opacity: 1 !important; }
/* a head after a page transition takes 0.8 s, not 1.1 s */
html.pv-internal.pv-anim :is(.svc-page-head, .ec-hero-in, .ai-hero-head, .ab-hero > .wrap, .quote-intro-block, .priv-head, .cs-head > .wrap, .bi-hero-copy) > * { animation-duration: .75s; }
/* the first screen's media belongs to the head's timeline: the AI page's call cards and the first service row rose
   nowhere while their headings arrived (they sat there first). From .01 so the LCP is still the first paint. */
@keyframes pv-media-up { from { opacity: .01; transform: translateY(14px); } to { opacity: 1; transform: none; } }
html.pv-anim .ai-hero .media-strip { animation: pv-media-up .8s var(--ease-out, cubic-bezier(.16,1,.3,1)) calc(var(--pv-head-d) + 300ms) both; }
html.pv-anim #svcRows > .svc-rows-vline + .svc-row { animation: pv-media-up .8s var(--ease-out, cubic-bezier(.16,1,.3,1)) calc(var(--pv-head-d) + 300ms) both; }
/* phone menu: the circle reaches exactly the far corner (hypot), opens on a quick-out curve and closes faster;
   150vmax overshot, so the last third of each move changed nothing on screen */
html.pv-anim body .menu-ov { clip-path: circle(0 at calc(100% - 38px) 32px); transition: clip-path .32s cubic-bezier(.4,0,1,1), visibility 0s .32s; }
html.pv-anim body .menu-ov.open { clip-path: circle(150vmax at calc(100% - 38px) 32px); transition: clip-path .42s cubic-bezier(.32,.72,0,1), visibility 0s; }
@supports (clip-path: circle(hypot(3px, 4px))) {
  html.pv-anim body .menu-ov.open { clip-path: circle(hypot(100vw, 100vh) at calc(100% - 38px) 32px); }
}
html.pv-anim body .menu-ov.open a { transition-delay: .12s; }
html.pv-anim body .menu-ov.open a:nth-child(2) { transition-delay: calc(.12s + 30ms * 1); }
html.pv-anim body .menu-ov.open a:nth-child(3) { transition-delay: calc(.12s + 30ms * 2); }
html.pv-anim body .menu-ov.open a:nth-child(4) { transition-delay: calc(.12s + 30ms * 3); }
html.pv-anim body .menu-ov.open a:nth-child(5) { transition-delay: calc(.12s + 30ms * 4); }
html.pv-anim body .menu-ov.open a:nth-child(6) { transition-delay: calc(.12s + 30ms * 5); }
/* the bar crossing a cream section: its text changes colour at the midpoint of the background's fade instead of
   fading through a grey that matched the grey background for a few frames */
html body .topbar:not(.nav-hidden) { transition: background-color var(--t-fast) var(--ease), color 0s linear .16s; }
html body .topbar.ready:not(.nav-hidden) { transition: transform .38s var(--ease-out), opacity .14s linear, background-color var(--t-fast) var(--ease), color 0s linear .16s; }
/* home: the case frames scroll to their anchor below the bar */
#wkGrid > li { scroll-margin-top: calc(var(--bar-h) + 24px); }
/* home / about FAQ: the answer's 22 px bottom padding counted toward the collapsing 0fr track, so the close stopped
   22 px short and then snapped when the details element shut; the gap is a spacer inside the clipped box instead */
html body .faq .faq-a > p{padding-bottom:0}
html body .faq .faq-a > p::after{content:"";display:block;height:22px}
/* "All services" had no hover at all */
html body .sg-all{transition:text-underline-offset .2s var(--ease-out),text-decoration-thickness .2s var(--ease-out)}
@media (hover:hover){ html body .sg-all:hover{text-underline-offset:7px;text-decoration-thickness:2px} }
/* the wizard's per-service questions appear with a short fade instead of in one frame */
@keyframes pv-opt-in { from { opacity: 0; transform: translateY(-4px); } }
html.pv-anim .svc-opt:not([hidden]) { animation: pv-opt-in .28s var(--ease-out, cubic-bezier(.16,1,.3,1)); }

/* ============================================================================================
   b126 r16 — design and copy (night jury)
   ============================================================================================ */
/* every inner page's head starts at the same height under the bar: the eyebrow sat anywhere from 116 px (AI) to
   195 px (privacy) at 1440. One rule: the bar plus 44-54 px. */
html body :is(.svc-page, .wd-hero, .ec-hero, .ai-hero, .bi-hero, .quote-page, .priv-page, .cs-head, .ab-hero){
  padding-top:calc(var(--bar-h) + clamp(44px,4vw,54px));
}
/* home, "See it working": the three links were inline-blocks with 10 px padding, so the paragraph's lines stood
   46 px apart on a phone. Inline again; the padding still enlarges the tap area without moving the lines. */
html body .sg-proof a{display:inline;padding-block:10px;margin-block:-10px;min-height:0;-webkit-box-decoration-break:clone;box-decoration-break:clone}
/* brand identity: the ask's two hairlines ran past the content column into the window's edges (x 10-1430) */
html body .bi-ctaline-in{border-block:0;position:relative}
html body .bi-ctaline-in::before, html body .bi-ctaline-in::after{content:"";position:absolute;left:var(--pad-x);right:var(--pad-x);height:1px;background:var(--line-strong)}
html body .bi-ctaline-in::before{top:0}
html body .bi-ctaline-in::after{bottom:0}
/* about: the work cards' meta sat 10 px right of the titles above it */
html body .ab-work-meta{display:block;margin:4px 0 0}
/* case pages: "What shipped" sat on its grid with no gap */
html body .cs-screens .label + .cs-list, html body .cs-screens h2.label + *{margin-top:28px}
/* phones: the single phone mockup on web design and ecommerce sat flush left with a dead strip beside it */
@media (max-width:767px){ html body .pv-state-proof--phone{margin-inline:auto} }
/* headings never end on one word; paragraphs avoid a lone last word */
html body main :is(h1, h2, h3){text-wrap:balance}
html body main :is(p, li, dd){text-wrap:pretty}
/* cookie card on wide screens: on the page's left content edge, not 16 px from the window */
@media (min-width:1024px){
  html body .pv-consent{left:max(var(--pv-gutter,16px), calc((100% - 1420px) / 2 + var(--pad-x)))}
}
/* phones: the bar's pill and the case tour's play disc were under the 44 px target */
@media (pointer:coarse){
  html body .topbar a.tb-cta{min-height:44px}
  html body .sbp-disc{min-width:44px;min-height:44px}
}
/* one pill size on every page: 52 px on home, 64 px (xl) on the service, about and case pages, 46 on the 404 —
   now one 56 px pill with 16 px words everywhere */
html body .btn, html body .btn--xl{padding:17px 28px;font-size:var(--fs-b);line-height:1.3}
/* the company's registry line (art. 206 KSH), under the studio line in the footer */
html body footer.site-foot .foot-reg{margin-top:12px;font-size:var(--fs-s);color:var(--muted-on-dark);font-variant-numeric:tabular-nums}
/* /privacy: a contents list beside the text on wide screens (the page was one 590 px column with the right half
   empty); on phones it sits under the head as a plain list */
html body .priv-toc{margin:0 0 clamp(36px,4.5vw,60px)}
html body .priv-toc ol{list-style:none;margin:12px 0 0;padding:0;display:grid;gap:10px}
html body .priv-toc a{color:var(--gray-3);text-decoration:underline;text-decoration-color:var(--line-strong);text-underline-offset:4px;transition:color var(--t-micro) var(--ease),text-decoration-color var(--t-micro) var(--ease)}
html body .priv-toc a:hover, html body .priv-toc a:focus-visible{color:var(--white);text-decoration-color:currentColor}
html body .priv-sec h2{scroll-margin-top:calc(var(--bar-h) + 24px)}
@media (min-width:1100px){
  html body .priv-page > .wrap{display:grid;grid-template-columns:minmax(0,1fr) minmax(220px,300px);column-gap:clamp(48px,6vw,120px);align-items:start}
  html body .priv-page .priv-head{grid-column:1 / -1}
  html body .priv-page .priv-body{grid-column:1;grid-row:2}
  html body .priv-page .priv-toc{grid-column:2;grid-row:2;position:sticky;top:calc(var(--bar-h) + 32px);margin:0;padding-left:24px;border-left:1px solid var(--line)}
}
/* one reveal direction for pictures: the service pages' photographs opened from the bottom up while the home
   frames' curtain uncovers from the top down; both now travel top to bottom, like the headings' wipe */
@keyframes bi-photo-in { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0); } }
@media (prefers-reduced-motion: no-preference) {
  html.js body .photo:not(.in) img { clip-path: inset(0 0 100% 0); }
}
/* b126 r22: the last phone targets under 44 px tall (night jury): the case pages' back and live-site links, the case
   caption's domain, the premium page's quiet quote link and the privacy contents. Padding, not size: the hit area
   grows and nothing on the page moves (negative margins on the flex and block ones, inline padding on the rest). */
@media (pointer:coarse){
  html body a.cs-back{padding-block:12px;margin-block:-12px}
  html body a.pp-quiet{padding-block:11px;margin-block:-11px}
  html body a.cs-domain,html body .priv-toc a{padding-block:12px}
}
/* b126 r22: a section head that fades in as a block (.rv) and whose heading also wipes in (.pv-h, motion.js) rose twice,
   28 px and .3 em stacked (night jury, home "Template sites look like templates"). The block keeps its fade; the
   heading alone carries the rise, as on every other page. */
html.pv-anim .rv:has(> h2.pv-h){transform:none}
/* b126 r22: /services/web-design's first screen ends in "One brief. Four real sites." and the two machines. They were
   painted flat at first paint, before the page's own h1 had arrived, and the machines' pictures popped in one frame
   (night jury). They are now the head's last beats, like the first service row on /services. From .01, so the LCP
   (the phone's picture) is still counted at first paint. */
html.pv-anim .wd-preview-head { animation: pv-media-up .8s var(--ease-out, cubic-bezier(.16,1,.3,1)) calc(var(--pv-head-d) + 300ms) both; }
html.pv-anim .wd-preview .wd-machines { animation: pv-media-up .8s var(--ease-out, cubic-bezier(.16,1,.3,1)) calc(var(--pv-head-d) + 390ms) both; }
/* b126 r22: the setup rows on /services/ai-assistants left one word on its own line ("calls forward to / it",
   "must never / say", "written for / you"). Balanced short lines instead. */
html body .mock-row :is(.mock-k,.mock-v){text-wrap:pretty}
